merge: sync orchestration before queue 00001KVSKJ0EA

This commit is contained in:
Keisuke Hirata 2026-06-23 15:47:17 +09:00
commit 8daf9eacb7
No known key found for this signature in database
25 changed files with 1453 additions and 394 deletions

View File

@ -1,8 +1,8 @@
--- ---
title: 'Generate Workspace web TypeScript types from protocol crate' title: 'Generate Workspace web TypeScript types from protocol crate'
state: 'inprogress' state: 'closed'
created_at: '2026-06-23T05:13:22Z' created_at: '2026-06-23T05:13:22Z'
updated_at: '2026-06-23T05:42:14Z' updated_at: '2026-06-23T06:22:01Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-23T05:40:01Z' queued_at: '2026-06-23T05:40:01Z'

View File

@ -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<T>` + `skip_serializing_if` fields は TS で optional field ではなく required nullable に出る。将来 UI が該当 field を使う際は注意。

View File

@ -126,3 +126,267 @@ Next action:
- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。 - Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。
--- ---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:09:42Z -->
## 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。
---
<!-- event: comment author: yoi-orchestrator at: 2026-06-23T06:10:31Z -->
## 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。
---
<!-- event: review author: yoi-reviewer-00001KVSEBF56-r1 at: 2026-06-23T06:16:54Z status: approve -->
## 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<T>` 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`
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:20:49Z -->
## 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`
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-23T06:21:01Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval、Orchestrator worktree への統合、protocol tests/drift check/wasm no-default check、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。
---
<!-- event: state_changed author: hare at: 2026-06-23T06:21:19Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-23T06:21:19Z status: 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<T>` + `skip_serializing_if` fields は TS で optional field ではなく required nullable に出る。将来 UI が該当 field を使う際は注意。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:22:01Z -->
## 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。
---

View File

@ -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"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'TUI Dashboard の冗長な key hints と selected-row 状態表示を削る' title: 'TUI Dashboard の冗長な key hints と selected-row 状態表示を削る'
state: 'queued' state: 'closed'
created_at: '2026-06-23T05:40:56Z' created_at: '2026-06-23T05:40:56Z'
updated_at: '2026-06-23T06:08:42Z' updated_at: '2026-06-23T06:33:21Z'
assignee: null assignee: null
readiness: 'implementation_ready' readiness: 'implementation_ready'
risk_flags: ['tui-ux', 'terminal-layout'] risk_flags: ['tui-ux', 'terminal-layout']

View File

@ -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 とした。

View File

@ -13,4 +13,294 @@ LocalTicketBackend によって作成されました。
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-23T06:11:11Z -->
## 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。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-23T06:11:22Z from: queued to: inprogress reason: human_authorized_unblocked_dashboard_hint_cleanup field: state -->
## State changed
Human authorized queue routing from Workspace Dashboard. Ticket has concrete Dashboard render acceptance criteria and no recorded blockers, so Orchestrator accepts implementation.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:12:36Z -->
## 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。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:28:12Z -->
## 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。
---
<!-- event: comment author: yoi-orchestrator at: 2026-06-23T06:28:54Z -->
## 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。
---
<!-- event: review author: yoi-reviewer-00001KVSFXY88-r1 at: 2026-06-23T06:31:21Z status: approve -->
## 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`
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:32:25Z -->
## 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`
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-23T06:32:37Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval、Orchestrator worktree への統合、focused Dashboard / workspace_panel tests、Ticket doctor が完了したため `done` に遷移する。
---
<!-- event: state_changed author: hare at: 2026-06-23T06:32:49Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-23T06:32:49Z status: 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 とした。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:33:21Z -->
## 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。
--- ---

View File

@ -1,8 +1,8 @@
--- ---
title: 'Improve Workspace web ticket Kanban grouping and lazy rows' title: 'Improve Workspace web ticket Kanban grouping and lazy rows'
state: 'inprogress' state: 'closed'
created_at: '2026-06-23T05:50:36Z' created_at: '2026-06-23T05:50:36Z'
updated_at: '2026-06-23T06:06:45Z' updated_at: '2026-06-23T06:16:10Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-23T05:53:22Z' queued_at: '2026-06-23T05:53:22Z'

View File

@ -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 は追加していない。

View File

@ -206,3 +206,167 @@ Reviewer focus:
Orchestrator will wait for reviewer verdict before integration。 Orchestrator will wait for reviewer verdict before integration。
--- ---
<!-- event: review author: yoi-reviewer-00001KVSGFM65-r1 at: 2026-06-23T06:13:34Z status: approve -->
## 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 `<span class="ticket-state">{ticket.state}</span>`
- Component boundary:
- Kanban logic and row rendering moved into `RepositoryTicketKanban.svelte`
- `WorkspacePage.svelte` now delegates with `<RepositoryTicketKanban tickets={repositoryTickets} />`
- 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 tickets 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`
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:15:01Z -->
## 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`
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-23T06:15:09Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval、Orchestrator worktree への統合、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。
---
<!-- event: state_changed author: hare at: 2026-06-23T06:15:22Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-23T06:15:22Z status: 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 は追加していない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-23T06:16:10Z -->
## 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。
---

23
Cargo.lock generated
View File

@ -3035,6 +3035,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"ts-rs",
"uuid", "uuid",
] ]
@ -4696,6 +4697,28 @@ dependencies = [
"toml", "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]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"

View File

@ -4,8 +4,14 @@ version = "0.1.0"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
[features]
default = ["stream"]
stream = ["dep:tokio"]
typescript = ["dep:ts-rs"]
[dependencies] [dependencies]
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } 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"] } uuid = { workspace = true, features = ["serde"] }

View File

@ -0,0 +1,16 @@
#[cfg(feature = "typescript")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
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);
}

View File

@ -1,4 +1,7 @@
#[cfg(feature = "stream")]
pub mod stream; pub mod stream;
#[cfg(feature = "typescript")]
pub mod typescript;
use std::path::PathBuf; use std::path::PathBuf;
@ -21,6 +24,7 @@ fn is_false(value: &bool) -> bool {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "snake_case")] #[serde(tag = "method", content = "params", rename_all = "snake_case")]
pub enum Method { pub enum Method {
Run { Run {
@ -103,6 +107,7 @@ pub enum Method {
/// delivery (e.g. `TurnEnded` arriving after `ShutDown` for the same /// delivery (e.g. `TurnEnded` arriving after `ShutDown` for the same
/// child Pod). /// child Pod).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum PodEvent { pub enum PodEvent {
/// Child finished one turn and is back to IDLE. /// 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 /// placeholder into the LLM context so neither user nor LLM is blind to
/// the dropped intent. /// the dropped intent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum Segment { pub enum Segment {
/// Free-form text. The fallback every client can produce. /// Free-form text. The fallback every client can produce.
@ -266,6 +272,7 @@ impl Method {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "event", content = "data", rename_all = "snake_case")] #[serde(tag = "event", content = "data", rename_all = "snake_case")]
pub enum Event { pub enum Event {
/// A user input message was accepted, persisted as /// A user input message was accepted, persisted as
@ -294,6 +301,7 @@ pub enum Event {
/// One event per `LogEntry::SystemItem` commit. Disk-side and /// One event per `LogEntry::SystemItem` commit. Disk-side and
/// wire-side are 1:1. /// wire-side are 1:1.
SystemItem { SystemItem {
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
item: serde_json::Value, item: serde_json::Value,
}, },
/// A new self-driving cycle has begun (IDLE → active transition). /// A new self-driving cycle has begun (IDLE → active transition).
@ -453,6 +461,7 @@ pub enum Event {
/// role-specific entry events (`SegmentRotated` / `SystemItem`) — /// role-specific entry events (`SegmentRotated` / `SystemItem`) —
/// there is no generic "every committed entry" broadcast. /// there is no generic "every committed entry" broadcast.
Snapshot { Snapshot {
#[cfg_attr(feature = "typescript", ts(type = "Array<unknown>"))]
entries: Vec<serde_json::Value>, entries: Vec<serde_json::Value>,
greeting: Greeting, greeting: Greeting,
#[serde(default)] #[serde(default)]
@ -471,6 +480,7 @@ pub enum Event {
/// ///
/// Payload is the JSON form of `session_store::LogEntry::SegmentStart`. /// Payload is the JSON form of `session_store::LogEntry::SegmentStart`.
SegmentRotated { SegmentRotated {
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
entry: serde_json::Value, entry: serde_json::Value,
}, },
/// Current Pod controller status. Broadcast on every controller-level /// 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 /// A rewind has truncated the authoritative session. `entries` is the
/// retained session-log prefix clients should use to reseed display state. /// retained session-log prefix clients should use to reseed display state.
RewindApplied { RewindApplied {
#[cfg_attr(feature = "typescript", ts(type = "Array<unknown>"))]
entries: Vec<serde_json::Value>, entries: Vec<serde_json::Value>,
input: Vec<Segment>, input: Vec<Segment>,
summary: RewindSummary, summary: RewindSummary,
@ -503,14 +514,17 @@ pub enum Event {
/// crate can evolve discovery fields without introducing a protocol /// crate can evolve discovery fields without introducing a protocol
/// dependency on session-store. /// dependency on session-store.
PodsListed { PodsListed {
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
pods: serde_json::Value, pods: serde_json::Value,
}, },
/// Reply to `Method::RestorePod`. /// Reply to `Method::RestorePod`.
PodRestored { PodRestored {
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
result: serde_json::Value, result: serde_json::Value,
}, },
/// Reply to `Method::RegisterPeer`. /// Reply to `Method::RegisterPeer`.
PeerRegistered { PeerRegistered {
#[cfg_attr(feature = "typescript", ts(type = "unknown"))]
result: serde_json::Value, result: serde_json::Value,
}, },
Alert(Alert), Alert(Alert),
@ -530,6 +544,7 @@ pub enum Event {
/// `new_segment_id` is the UUID of the freshly created session that /// `new_segment_id` is the UUID of the freshly created session that
/// replaced the old history. /// replaced the old history.
CompactDone { CompactDone {
#[cfg_attr(feature = "typescript", ts(type = "string"))]
new_segment_id: uuid::Uuid, new_segment_id: uuid::Uuid,
}, },
/// Compaction failed. The session is unchanged. /// Compaction failed. The session is unchanged.
@ -546,6 +561,7 @@ pub enum Event {
/// surfaced to the person driving the client. Keep messages short and /// surfaced to the person driving the client. Keep messages short and
/// human-readable. /// human-readable.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct Alert { pub struct Alert {
pub level: AlertLevel, pub level: AlertLevel,
pub source: AlertSource, pub source: AlertSource,
@ -555,6 +571,7 @@ pub struct Alert {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct MemoryWorkerEvent { pub struct MemoryWorkerEvent {
pub worker: String, pub worker: String,
pub status: String, pub status: String,
@ -568,6 +585,7 @@ pub struct MemoryWorkerEvent {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AlertLevel { pub enum AlertLevel {
Warn, Warn,
@ -575,6 +593,7 @@ pub enum AlertLevel {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum AlertSource { pub enum AlertSource {
Pod, Pod,
@ -591,6 +610,7 @@ pub enum AlertSource {
/// nailed down here so the TUI side can ship without waiting for /// nailed down here so the TUI side can ship without waiting for
/// the memory / workflow tickets. /// the memory / workflow tickets.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum CompletionKind { pub enum CompletionKind {
File, File,
@ -605,6 +625,7 @@ pub enum CompletionKind {
/// keep a trailing `/` after a directory selection so the user can /// keep a trailing `/` after a directory selection so the user can
/// drill in without re-typing the prefix. /// drill in without re-typing the prefix.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct CompletionEntry { pub struct CompletionEntry {
pub value: String, pub value: String,
#[serde(default)] #[serde(default)]
@ -612,12 +633,15 @@ pub struct CompletionEntry {
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct RewindTargetId { pub struct RewindTargetId {
#[cfg_attr(feature = "typescript", ts(type = "string"))]
pub segment_id: uuid::Uuid, pub segment_id: uuid::Uuid,
pub user_input_entry_index: usize, pub user_input_entry_index: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct RewindTarget { pub struct RewindTarget {
pub id: RewindTargetId, pub id: RewindTargetId,
pub expected_head_entries: usize, pub expected_head_entries: usize,
@ -633,6 +657,7 @@ pub struct RewindTarget {
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct RewindSummary { pub struct RewindSummary {
pub truncated_to_entries: usize, pub truncated_to_entries: usize,
pub discarded_entries: usize, pub discarded_entries: usize,
@ -647,6 +672,7 @@ pub struct RewindSummary {
/// history. Finalized assistant items continue to come from ordinary snapshot /// history. Finalized assistant items continue to come from ordinary snapshot
/// entries. /// entries.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct InFlightSnapshot { pub struct InFlightSnapshot {
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<InFlightBlock>, pub blocks: Vec<InFlightBlock>,
@ -659,6 +685,7 @@ impl InFlightSnapshot {
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum InFlightBlock { pub enum InFlightBlock {
Text { Text {
@ -681,6 +708,7 @@ pub enum InFlightBlock {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum InFlightToolCallState { pub enum InFlightToolCallState {
#[default] #[default]
@ -701,6 +729,7 @@ impl InFlightToolCallState {
/// transmitted alongside `Event::Snapshot` so clients don't need /// transmitted alongside `Event::Snapshot` so clients don't need
/// their own view of the manifest. /// their own view of the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct Greeting { pub struct Greeting {
pub pod_name: String, pub pod_name: String,
pub cwd: String, pub cwd: String,
@ -721,6 +750,7 @@ pub struct Greeting {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum PodStatus { pub enum PodStatus {
#[default] #[default]
@ -730,6 +760,7 @@ pub enum PodStatus {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TurnResult { pub enum TurnResult {
Finished, Finished,
@ -743,6 +774,7 @@ pub enum TurnResult {
/// notify message, pod event body) is delivered by the immediately /// notify message, pod event body) is delivered by the immediately
/// following Turn entry, not by the marker itself. /// following Turn entry, not by the marker itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum InvokeKind { pub enum InvokeKind {
/// `Method::Run` — a user submission. /// `Method::Run` — a user submission.
@ -762,6 +794,7 @@ pub enum InvokeKind {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RunResult { pub enum RunResult {
Finished, Finished,
@ -775,6 +808,7 @@ pub enum RunResult {
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ErrorCode { pub enum ErrorCode {
AlreadyRunning, AlreadyRunning,
@ -796,6 +830,7 @@ pub enum ErrorCode {
/// A single allow or deny rule inside a scope configuration. /// A single allow or deny rule inside a scope configuration.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct ScopeRule { pub struct ScopeRule {
/// Target path. Must be absolute by the time a `Scope` is built from /// Target path. Must be absolute by the time a `Scope` is built from
/// this rule — relative paths are resolved per-layer against the /// 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 /// everything below); deny rules cap the effective level **strictly
/// below** the stated level. /// below** the stated level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Permission { pub enum Permission {
Read, Read,

View File

@ -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::<AlertLevel>(&cfg, &mut output);
push_decl::<AlertSource>(&cfg, &mut output);
push_decl::<CompletionKind>(&cfg, &mut output);
push_decl::<PodStatus>(&cfg, &mut output);
push_decl::<TurnResult>(&cfg, &mut output);
push_decl::<InvokeKind>(&cfg, &mut output);
push_decl::<RunResult>(&cfg, &mut output);
push_decl::<ErrorCode>(&cfg, &mut output);
push_decl::<Permission>(&cfg, &mut output);
push_decl::<InFlightToolCallState>(&cfg, &mut output);
push_decl::<ScopeRule>(&cfg, &mut output);
push_decl::<CompletionEntry>(&cfg, &mut output);
push_decl::<RewindTargetId>(&cfg, &mut output);
push_decl::<RewindTarget>(&cfg, &mut output);
push_decl::<RewindSummary>(&cfg, &mut output);
push_decl::<InFlightBlock>(&cfg, &mut output);
push_decl::<InFlightSnapshot>(&cfg, &mut output);
push_decl::<Greeting>(&cfg, &mut output);
push_decl::<Alert>(&cfg, &mut output);
push_decl::<MemoryWorkerEvent>(&cfg, &mut output);
push_decl::<Segment>(&cfg, &mut output);
push_decl::<PodEvent>(&cfg, &mut output);
push_decl::<Method>(&cfg, &mut output);
push_decl::<Event>(&cfg, &mut output);
normalize_typescript(output)
}
fn normalize_typescript(output: String) -> String {
let mut lines = output.lines().map(str::trim_end).collect::<Vec<_>>();
while lines.last() == Some(&"") {
lines.pop();
}
lines.join("\n") + "\n"
}
fn push_decl<T: TS>(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`"
);
}
}

View File

@ -1437,31 +1437,6 @@ impl DashboardApp {
} }
} }
pub(crate) fn selected_open_disabled_reason(&self) -> Option<String> {
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) { pub(crate) fn select_next(&mut self) {
let visible = visible_panel_keys(&self.panel, &self.list); let visible = visible_panel_keys(&self.panel, &self.list);
if visible.is_empty() { 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 { fn selected_ticket_notice(row: Option<&PanelRow>) -> String {
match row { match row {
Some(row) if row.is_ticket_action() => { Some(row) if row.is_ticket_action() => {

View File

@ -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) { pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect) {
let guidance = if app frame.render_widget(Paragraph::new(title_line(app)), area);
.panel }
.composer
.is_available(ComposerTarget::TicketIntake) pub(super) fn title_line(app: &DashboardApp) -> Line<'static> {
{ let mut spans = vec![Span::styled(
" Row selection: blank Enter opens/dispatches · text Enter uses target · Tab target" "workspace dashboard",
} else if app.panel.header.ticket_configured { Style::default().add_modifier(Modifier::BOLD),
" 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)),
];
if let Some(companion) = &app.panel.header.companion { if let Some(companion) = &app.panel.header.companion {
spans.push(Span::styled( spans.push(Span::styled(
" · companion ", " · companion ",
@ -101,7 +91,7 @@ pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect)
orchestrator_status_style(orchestrator.status), 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 { 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); frame.render_widget(Paragraph::new(target_status_line(app)), area);
} }
pub(super) fn target_status_line(app: &DashboardApp) -> Line<'static> { pub(super) fn target_status_line(_app: &DashboardApp) -> Line<'static> {
if !app.composer_is_blank() { Line::from(Span::raw(""))
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 draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRender, area: Rect) { 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 { pub(super) fn actionbar_left_text(app: &DashboardApp) -> String {
if app.sending && app.composer_target() == ComposerTarget::TicketIntake { if app.sending && app.composer_target() == ComposerTarget::TicketIntake {
"launching Ticket Intake…".to_string() "launching Ticket Intake…".to_string()
@ -927,52 +713,20 @@ pub(super) fn actionbar_left_text(app: &DashboardApp) -> String {
Some(notice) => format!("{notice} Refreshing workspace…"), Some(notice) => format!("{notice} Refreshing workspace…"),
None => "Refreshing workspace…".to_string(), 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() { } else if let Some(notice) = app.notice.as_deref() {
notice.to_string() notice.to_string()
} else if let Some(reason) = app.selected_open_disabled_reason() {
reason
} else { } else {
match app.composer_target() { String::new()
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()
}
}
}
} }
} }
pub(super) fn actionbar_right_text(app: &DashboardApp) -> &'static str { pub(super) fn actionbar_right_text(app: &DashboardApp) -> &'static str {
if app.panel_diagnostic_open { if app.panel_diagnostic_open {
"F2/Esc close details Ctrl+C quit" "F2/Esc close details"
} else if app.panel_diagnostic.is_some() { } 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" "F2 details"
} 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"
} else { } else {
"↑/↓ select row Enter selected row Esc clear selection Left/Right cursor Ctrl+C quit" ""
} }
} }

View File

@ -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_panel_row().unwrap().title, "Queued");
assert_eq!(app.selected_ticket_action(), Some(NextUserAction::Wait)); 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!( assert!(matches!(
app.handle_key(key(KeyCode::Enter)), app.handle_key(key(KeyCode::Enter)),
DashboardAction::DispatchTicketAction(request) if request.ticket_id == "TICKET-2" DashboardAction::DispatchTicketAction(request) if request.ticket_id == "TICKET-2"
@ -1152,7 +1154,7 @@ fn mouse_click_does_not_override_existing_composer_keyboard_behavior() {
} }
#[test] #[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")); let mut panel = WorkspacePanelViewModel::empty(Path::new("test"));
panel.header.companion = Some(CompanionPanelState::new( panel.header.companion = Some(CompanionPanelState::new(
"yoi", "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 actionbar_right = actionbar_right_text(&app);
let target_status = plain_line(&target_status_line(&app)); let target_status = plain_line(&target_status_line(&app));
assert!(actionbar_left.contains("Companion target: Enter sends composer text")); assert_eq!(actionbar_left, "");
assert!(actionbar_right.contains("Enter composer target")); assert_eq!(actionbar_right, "");
assert!(!actionbar_left.contains("Queue")); assert_eq!(target_status, "");
assert!(!actionbar_right.contains("selected row")); let selected_title =
assert!(target_status.contains("composer target Companion")); plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]);
assert!(target_status.contains("draft Enter send composer text to workspace Companion")); assert!(selected_title.starts_with("▶ ready"));
assert!(target_status.contains("row selection waits until composer is blank"));
assert!(!target_status.contains("blank Enter Queue"));
} }
#[test] #[test]
@ -1529,7 +1529,6 @@ fn dashboard_idle_live_selected_target_is_open_eligible() {
let app = test_app(vec![live_info("idle", PodStatus::Idle)]); let app = test_app(vec![live_info("idle", PodStatus::Idle)]);
assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow);
assert!(app.selected_open_disabled_reason().is_none());
} }
#[test] #[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] #[test]
fn panel_ticket_rows_render_state_title_then_detail_line() { fn panel_ticket_rows_render_state_title_then_detail_line() {
let row = panel_test_ticket_row( let row = panel_test_ticket_row(
@ -1725,7 +1753,7 @@ fn panel_ticket_intake_child_rows_render_as_indented_single_line() {
} }
#[test] #[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 ticket_id = "00001TICKET";
let pod_name = "intake-live"; let pod_name = "intake-live";
let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); 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)); let status = plain_line(&target_status_line(&app));
assert!(status.contains("selected Intake Pod live")); assert_eq!(status, "");
assert!(status.contains("Ticket 00001TICKET"));
assert!(status.contains("blank Enter open/attach")); let title_line = plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]);
assert!(!status.contains("selected Ticket")); assert!(title_line.starts_with(" ▶ live"));
assert!(!status.contains("selected Pod live")); assert!(title_line.contains("Intake"));
} }
#[test] #[test]
@ -1828,15 +1856,12 @@ fn dashboard_running_paused_and_stopped_targets_are_open_eligible() {
app.ensure_selection_visible(); app.ensure_selection_visible();
assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow);
assert!(app.selected_open_disabled_reason().is_none());
app.select_next(); app.select_next();
assert_eq!(app.list.selected_entry().unwrap().name, "paused"); assert_eq!(app.list.selected_entry().unwrap().name, "paused");
assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow);
assert!(app.selected_open_disabled_reason().is_none());
app.select_next(); app.select_next();
assert_eq!(app.list.selected_entry().unwrap().name, "stopped"); assert_eq!(app.list.selected_entry().unwrap().name, "stopped");
assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow);
assert!(app.selected_open_disabled_reason().is_none());
} }
#[test] #[test]

View File

@ -250,11 +250,11 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
store: "sqlite".to_string(), store: "sqlite".to_string(),
event_stream: ExtensionPointState { event_stream: ExtensionPointState {
status: "reserved".to_string(), status: "reserved".to_string(),
note: "No event stream is exposed in this bootstrap; route/state seams are reserved.".to_string(), note: "No browser-to-Pod socket path is exposed in this bootstrap; any future stream must be a Workspace server proxy that resolves Worker identity and enforces method allow/block boundaries.".to_string(),
}, },
host_worker_bridge: ExtensionPointState { host_worker_bridge: ExtensionPointState {
status: "read_only_local".to_string(), status: "read_only_local".to_string(),
note: "Local Hosts and Workers are exposed as a read-only bridge over existing Pod metadata; no scheduling or lifecycle control is implemented.".to_string(), note: "Local Hosts and Workers are exposed as a read-only bridge over existing Pod metadata; no direct Pod socket, scheduling, or lifecycle control is implemented.".to_string(),
}, },
}, },
})) }))

View File

@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter; filter = sourceFilter;
}; };
cargoHash = "sha256-dKkAFUfTAMxSRHq9iNmwRXjQVSBHQBtb0+v8VHkgAGM="; cargoHash = "sha256-M8cGY+eskFXSRjq3kBbRusflghvVKrWc1Pj50uKAlg8=";
depsExtraArgs = { depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint, # Older fetchCargoVendor utilities used crates.io's API download endpoint,

View File

@ -347,8 +347,7 @@
border-top: 0; border-top: 0;
} }
.runtime-card, .runtime-card {
.kanban-column {
padding: var(--space-4) 0 0; padding: var(--space-4) 0 0;
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
@ -423,36 +422,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
.kanban {
display: grid;
gap: var(--space-5);
grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
margin-top: var(--space-4);
}
.kanban-column h3 {
display: flex;
justify-content: space-between;
gap: var(--space-3);
color: var(--text-muted);
font-size: 0.9rem;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.kanban-column ul {
display: grid;
gap: var(--space-3);
margin: 0;
padding: 0;
list-style: none;
}
.kanban-column li {
padding-left: var(--space-3);
border-left: 2px solid var(--line);
}
.diagnostics { .diagnostics {
margin-top: var(--space-4); margin-top: var(--space-4);
} }

View File

@ -0,0 +1,123 @@
// @generated by `cargo run -p protocol --example generate_typescript --features typescript`
// Do not edit by hand. Rust DTO authority lives in `crates/protocol`.
// Large integer fields are JSON numbers on the wire and are emitted as TypeScript `number`.
export type AlertLevel = "warn" | "error";
export type AlertSource = "pod" | "worker" | "compactor" | "agents_md";
export type CompletionKind = "file" | "knowledge" | "workflow";
export type PodStatus = "idle" | "running" | "paused";
export type TurnResult = "finished" | "paused";
export type InvokeKind = "user_send" | "notify" | "pod_event" | "system_reminder" | "wakeup";
export type RunResult = "finished" | "paused" | "limit_reached" | "rolled_back";
export type ErrorCode = "already_running" | "not_running" | "not_paused" | "provider_error" | "tool_error" | "invalid_request" | "internal";
export type Permission = "read" | "write";
export type InFlightToolCallState = "pending" | "streaming_args" | "done";
export type ScopeRule = {
/**
* Target path. Must be absolute by the time a `Scope` is built from
* this rule relative paths are resolved per-layer against the
* manifest file's directory (cwd for overlay layers) before cascade
* merge.
*/
target: string,
/**
* Permission level this rule grants (allow) or caps strictly below
* (deny).
*/
permission: Permission,
/**
* When `false`, the rule only matches the target itself and its
* direct children. Defaults to `true`.
*/
recursive: boolean, };
export type CompletionEntry = { value: string, is_dir: boolean, };
export type RewindTargetId = { segment_id: string, user_input_entry_index: number, };
export type RewindTarget = { id: RewindTargetId, expected_head_entries: number, truncate_entries: number, turn_index: number, timestamp_ms: number | null, preview: string, eligible: boolean, disabled_reason: string | null, warning: string | null, };
export type RewindSummary = { truncated_to_entries: number, discarded_entries: number, tool_side_effect_warning: boolean, };
export type InFlightBlock = { "kind": "text", text: string, finished?: boolean, } | { "kind": "thinking", text: string, finished?: boolean, } | { "kind": "tool_call", id: string, name: string, args: string, state?: InFlightToolCallState, };
export type InFlightSnapshot = { blocks?: Array<InFlightBlock>, };
export type Greeting = { pod_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array<string>,
/**
* 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<ScopeRule>, };
export type Method = { "method": "run", "params": { input: Array<Segment>, } } | { "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<Segment>, } } | { "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<unknown>, 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<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { entries: Array<unknown>, input: Array<Segment>, 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" };

View File

@ -0,0 +1,283 @@
<script lang="ts">
import type { RepositoryTicketsResponse, TicketSummary } from '$lib/workspace-sidebar/types';
const INITIAL_VISIBLE_ROWS = 30;
const VISIBLE_ROW_INCREMENT = 30;
const NEAR_BOTTOM_PX = 96;
type KanbanGroup = {
key: string;
label: string;
states: string[];
items: TicketSummary[];
};
type GroupMetadata = {
key: string;
label: string;
states: string[];
};
let { tickets }: { tickets: RepositoryTicketsResponse } = $props();
let visibleRowsByGroup = $state<Record<string, number>>({});
let groups = $derived(buildGroups(tickets.columns));
$effect(() => {
const groupKeys = new Set(groups.map((group) => group.key));
const nextVisibleRows = { ...visibleRowsByGroup };
let changed = false;
for (const group of groups) {
if (nextVisibleRows[group.key] === undefined) {
nextVisibleRows[group.key] = INITIAL_VISIBLE_ROWS;
changed = true;
}
}
for (const key of Object.keys(nextVisibleRows)) {
if (!groupKeys.has(key)) {
delete nextVisibleRows[key];
changed = true;
}
}
if (changed) {
visibleRowsByGroup = nextVisibleRows;
}
});
function buildGroups(columns: RepositoryTicketsResponse['columns']): KanbanGroup[] {
const groupsByKey = new Map<string, KanbanGroup>();
for (const column of columns) {
const metadata = groupMetadataForState(column.state);
let group = groupsByKey.get(metadata.key);
if (!group) {
group = {
key: metadata.key,
label: metadata.label,
states: metadata.states,
items: []
};
groupsByKey.set(metadata.key, group);
}
group.items.push(...column.items);
}
return Array.from(groupsByKey.values()).map((group) => ({
...group,
items: sortGroupItems(group.items)
}));
}
function groupMetadataForState(state: string): GroupMetadata {
if (state === 'planning' || state === 'ready') {
return {
key: 'ready-planning',
label: 'Ready / Planning',
states: ['ready', 'planning']
};
}
if (state === 'queued' || state === 'inprogress') {
return {
key: 'inprogress-queued',
label: 'In progress / Queued',
states: ['inprogress', 'queued']
};
}
if (state === 'other') {
return {
key: 'state:other',
label: 'Other states',
states: ['other']
};
}
return {
key: `state:${state}`,
label: state,
states: [state]
};
}
function sortGroupItems(items: TicketSummary[]): TicketSummary[] {
return items
.map((ticket, index) => ({ ticket, index }))
.sort((left, right) => {
const stateOrder = statePriority(left.ticket.state) - statePriority(right.ticket.state);
if (stateOrder !== 0) {
return stateOrder;
}
return left.index - right.index;
})
.map(({ ticket }) => ticket);
}
function statePriority(state: string): number {
if (state === 'ready' || state === 'inprogress') {
return 0;
}
if (state === 'planning' || state === 'queued') {
return 1;
}
return 2;
}
function visibleCount(groupKey: string): number {
return visibleRowsByGroup[groupKey] ?? INITIAL_VISIBLE_ROWS;
}
function visibleTickets(group: KanbanGroup): TicketSummary[] {
return group.items.slice(0, visibleCount(group.key));
}
function hasMore(group: KanbanGroup): boolean {
return visibleCount(group.key) < group.items.length;
}
function onGroupScroll(group: KanbanGroup, event: Event) {
if (!hasMore(group)) {
return;
}
const target = event.currentTarget;
if (!(target instanceof HTMLElement)) {
return;
}
const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
if (distanceFromBottom > NEAR_BOTTOM_PX) {
return;
}
visibleRowsByGroup = {
...visibleRowsByGroup,
[group.key]: Math.min(group.items.length, visibleCount(group.key) + VISIBLE_ROW_INCREMENT)
};
}
function formatDate(value: string | null | undefined): string {
return value ?? 'not recorded';
}
</script>
<div class="repository-ticket-kanban">
{#each groups as group (group.key)}
<article class="ticket-group" aria-labelledby={`${group.key}-heading`}>
<header class="ticket-group-heading">
<div>
<h3 id={`${group.key}-heading`}>{group.label}</h3>
<p>{group.states.join(' + ')}</p>
</div>
<span>{group.items.length}</span>
</header>
{#if group.items.length === 0}
<p class="muted">No tickets.</p>
{:else}
<div
class="ticket-list-scroll"
aria-label={`${group.label} tickets`}
onscroll={(event) => onGroupScroll(group, event)}
>
<ul class="ticket-list">
{#each visibleTickets(group) as ticket (ticket.id)}
<li class="ticket-row">
<div class="ticket-row-heading">
<strong>{ticket.title}</strong>
<span class="ticket-state">{ticket.state}</span>
</div>
<small><code>{ticket.id}</code> · updated {formatDate(ticket.updated_at)}</small>
</li>
{/each}
</ul>
{#if hasMore(group)}
<p class="lazy-note">Showing {visibleCount(group.key)} of {group.items.length}; scroll for more.</p>
{/if}
</div>
{/if}
</article>
{/each}
</div>
<style>
.repository-ticket-kanban {
display: grid;
gap: var(--space-5);
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
margin-top: var(--space-4);
}
.ticket-group {
min-width: 0;
padding: var(--space-4) 0 0;
border-top: 1px solid var(--line);
}
.ticket-group-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.ticket-group-heading h3 {
margin: 0;
color: var(--text-muted);
font-size: 0.9rem;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.ticket-group-heading p,
.ticket-group-heading span,
.lazy-note {
color: var(--text-faint);
font-size: 0.78rem;
}
.ticket-group-heading p,
.lazy-note {
margin: var(--space-1) 0 0;
}
.ticket-list-scroll {
max-height: 34rem;
overflow-y: auto;
padding-right: var(--space-2);
scrollbar-gutter: stable;
}
.ticket-list {
display: grid;
gap: var(--space-3);
margin: 0;
padding: 0;
list-style: none;
}
.ticket-row {
padding-left: var(--space-3);
border-left: 2px solid var(--line);
}
.ticket-row-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
}
.ticket-row-heading strong {
color: var(--text-strong);
}
.ticket-state {
flex: 0 0 auto;
color: var(--text-muted);
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
</style>

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import RepositoryTicketKanban from '$lib/workspace-pages/RepositoryTicketKanban.svelte';
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte'; import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
import type { import type {
Diagnostic, Diagnostic,
@ -355,25 +356,7 @@
Read-only grouping of canonical Ticket records. No drag/drop or lifecycle mutation is exposed. Read-only grouping of canonical Ticket records. No drag/drop or lifecycle mutation is exposed.
</p> </p>
{#if repositoryTickets} {#if repositoryTickets}
<div class="kanban"> <RepositoryTicketKanban tickets={repositoryTickets} />
{#each repositoryTickets.columns as column (column.state)}
<article class="kanban-column">
<h3>{column.state} <span>{column.items.length}</span></h3>
{#if column.items.length === 0}
<p class="muted">No tickets.</p>
{:else}
<ul>
{#each column.items as ticket (ticket.id)}
<li>
<strong>{ticket.title}</strong>
<small><code>{ticket.id}</code> · updated {formatDate(ticket.updated_at)}</small>
</li>
{/each}
</ul>
{/if}
</article>
{/each}
</div>
{:else if repositoryTicketsError} {:else if repositoryTicketsError}
<p class="error">{repositoryTicketsError}</p> <p class="error">{repositoryTicketsError}</p>
{:else} {:else}

View File

@ -1,3 +1,9 @@
export type {
Event as PodProtocolEvent,
Method as PodProtocolMethod,
Segment as PodProtocolSegment,
} from '$lib/generated/protocol';
export type ExtensionPoint = { export type ExtensionPoint = {
status: string; status: string;
note: string; note: string;