merge: sync orchestration before queue 00001KVSKJ0EA
This commit is contained in:
commit
8daf9eacb7
|
|
@ -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'
|
||||||
|
|
|
||||||
29
.yoi/tickets/00001KVSEBF56/resolution.md
Normal file
29
.yoi/tickets/00001KVSEBF56/resolution.md
Normal 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 を使う際は注意。
|
||||||
|
|
@ -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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
@ -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']
|
||||||
|
|
|
||||||
20
.yoi/tickets/00001KVSFXY88/resolution.md
Normal file
20
.yoi/tickets/00001KVSFXY88/resolution.md
Normal 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 とした。
|
||||||
|
|
@ -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。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
22
.yoi/tickets/00001KVSGFM65/resolution.md
Normal file
22
.yoi/tickets/00001KVSGFM65/resolution.md
Normal 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 は追加していない。
|
||||||
|
|
@ -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 ticket’s frontend-only scope and current test tooling, this is acceptable。
|
||||||
|
|
||||||
|
Reviewer validation:
|
||||||
|
- `git diff --check a6f9019e..6f68bb8d`: passed
|
||||||
|
- `git diff --check a6f9019e..HEAD`: passed
|
||||||
|
- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check && deno task build`: passed (`svelte-check found 0 errors and 0 warnings`; static build completed)
|
||||||
|
- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`)
|
||||||
|
- `nix build .#yoi --no-link`: passed
|
||||||
|
- `cargo test -p yoi-workspace-server`: not run; no backend/API changes were made。
|
||||||
|
|
||||||
|
Generated artifacts:
|
||||||
|
- Ignored validation artifacts present after review:
|
||||||
|
- `target/`
|
||||||
|
- `web/workspace/.svelte-kit/`
|
||||||
|
- `web/workspace/build/`
|
||||||
|
- `web/workspace/node_modules/`
|
||||||
|
- No generated artifacts are tracked。
|
||||||
|
|
||||||
|
Worktree status:
|
||||||
|
- Clean tracked state on `impl/00001KVSGFM65-kanban-lazy-rows` at `6f68bb8d`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 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
23
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"] }
|
||||||
|
|
|
||||||
16
crates/protocol/examples/generate_typescript.rs
Normal file
16
crates/protocol/examples/generate_typescript.rs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
97
crates/protocol/src/typescript.rs
Normal file
97
crates/protocol/src/typescript.rs
Normal 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`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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() => {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
} else if app.panel.header.ticket_configured {
|
|
||||||
" Row selection: blank Enter opens/dispatches · text Enter sends to Companion"
|
|
||||||
} else {
|
|
||||||
" Pod-centric view · Row selection: blank Enter opens · text Enter sends to Companion"
|
|
||||||
};
|
|
||||||
let mut spans = vec![
|
|
||||||
Span::styled(
|
|
||||||
"workspace dashboard",
|
"workspace dashboard",
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
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 {
|
} 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 {
|
|
||||||
"↑/↓ select row Enter selected row Esc clear selection Left/Right cursor Ctrl+C quit"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
web/workspace/src/lib/generated/protocol.ts
Normal file
123
web/workspace/src/lib/generated/protocol.ts
Normal 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" };
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user