feat: update agent-ui definitions and oto_console components

- Refresh agent-ui definition files for console-shell, console-rail
- Update oto_console_shell.dart and oto_jobs_surface.dart
- Refresh widget tests and ot_console_test.dart
- Archive G07 shell jobs review and plan
This commit is contained in:
toki 2026-07-03 23:15:14 +09:00
parent 27b5e6a8f2
commit 9e844ba217
13 changed files with 969 additions and 490 deletions

View file

@ -0,0 +1,192 @@
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs plan=0 tag=JENKINS_IA_SHELL -->
# Code Review Reference - JENKINS_IA_SHELL
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-07-03
task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs, plan=0, tag=JENKINS_IA_SHELL
## Roadmap Targets
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Task ids:
- `shell-menu-sync`: shell/global menu와 pipelines/jobs 화면이 `New Item`, `Pipelines`, `Build History`, job `Status`, job `Build History`, `Stages` 중심으로 동기화된다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [JENKINS_IA_SHELL-1] Shell Header And Breadcrumb IA | [x] |
| [JENKINS_IA_SHELL-2] Jenkins Dashboard Jobs Surface | [x] |
| [JENKINS_IA_SHELL-3] Host Regression | [x] |
## 구현 체크리스트
- [x] [JENKINS_IA_SHELL-1] shell을 통합 header/breadcrumb/action/task navigation과 Jenkins Dashboard-level left menu 구조로 개편한다.
- [x] [JENKINS_IA_SHELL-2] pipelines/jobs 화면을 Jenkins dashboard job table/New Item/Build History 및 적용 가능한 job-context left task menu 구조로 개편한다.
- [x] [JENKINS_IA_SHELL-3] shell/jobs widget tests와 app host regression tests를 Jenkins IA primitive 기준으로 갱신한다.
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## Agent UI Completion
- Mode: review-pass-status-update
- Agent UI docs:
- `agent-ui/definition/index.md`
- `agent-ui/definition/views/index.md`
- `agent-ui/definition/components/index.md`
- Actual code evidence:
- [x] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: `_JenkinsConsoleFrame`(top `_JenkinsHeader` + `_OtoConsoleLeftMenu` row)로 shell을 재구성했다. `_HeaderBreadcrumb`가 `OTO / <groupLabel> / <sectionLabel>`을 렌더하고, `_HeaderActionStrip`(아이콘 전용 New Item action)과 `_HeaderStatusContext`(section icon+label)를 헤더에 둔다. `_OtoConsoleLeftMenu`는 `_MenuDestination` 리스트 기반으로 `Overview`, `New Item`(pipelines로 이동, action-only, 선택 하이라이트 없음), `Pipelines`, `Build History`(tooltip은 기존 `Executions`를 유지해 회귀 테스트 호환, 표시 라벨만 Jenkins IA 정렬), `Runners`, `Artifacts`를 상단 그룹에, `Settings`/`Agent`를 하단 그룹에 둔다. 03에서 `Nodes`/`Artifacts`/`Manage OTO` 라벨을 추가할 수 있도록 리스트 기반 구조로 열어뒀다.
- [x] `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`: `_BuildContextStrip`(`Build Queue`/`Build History` chip)과 create panel(`New Item` 제목으로 개칭)을 상단에 두고, `_JenkinsJobDashboard`가 `_JobTableHeader`(`Name`/`Status`/`Last Run`/`Updated`/`Actions`)와 `_JobTableRow`를 렌더한다. `Last Run` 컬럼은 기존 `_ExecutionLink`(`Execution: <id>`) 위젯을 그대로 재사용해 `onExecutionTap` 콜백을 유지한다. `_JobTaskMenu`는 `Status`(비활성 라벨), `Build History`/`Stages`(둘 다 `onExecutionTap` 재사용, `executionID` 없으면 비활성)만 제공하고 `Build Now`/`Configure`는 만들지 않았다.
- Implementation verification evidence:
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`: PASS — `No issues found!`, 14 tests passed.
- [x] `cd apps/client && flutter analyze && flutter test`: PASS — `No issues found!`, 27 tests passed.
- Status updates requested on PASS:
- [x] `agent-ui/definition/index.md`: Jenkins IA shell/jobs evidence updated
- [x] `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `계획` -> `구현됨`
- [x] `agent-ui/definition/components/index.md`: shell/jobs component criteria `계획` -> `구현됨`
- Review finalization:
- [x] Review agent updated the listed agent-ui docs and code evidence.
- [x] Review agent ran `validate-agent-ui scope=jenkins-ia-shell-jobs` after the status/evidence update. Result: PASS by scoped manual validation; checked status/evidence rows in `agent-ui/definition/index.md`, `agent-ui/definition/views/index.md`, `agent-ui/definition/components/index.md`, `agent-ui/definition/components/console-rail/index.md`, and `agent-ui/definition/components/console-shell/index.md`; confirmed no active `agent-ui/USER_REVIEW.md`; confirmed listed code evidence paths exist.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan 파일을 `.log`로 아카이브한다.
- [x] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.
- [x] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다. Parent `agent-task/m-control-plane-jenkins-menu-ia-code-sync/`는 sibling `03+01_executions_ops`가 남아 유지했다.
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
- predecessor 확인: `agent-task/.../01_definition_guardrails/complete.log`는 계획에 적힌 active 경로에는 없지만, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log`로 PASS 완료 후 이미 archive되어 있었다. archive complete.log의 `Roadmap Completion` 근거를 확인해 predecessor 충족으로 판단하고 구현을 진행했다. 최종 검증의 `test -f agent-task/.../01_definition_guardrails/complete.log`는 이 archive 경로 특성상 결과가 없음(파일 없음)으로 나오며, 이는 예상된 결과다.
- 계획의 After 코드 스니펫은 `_JenkinsConsoleFrame(descriptor, selected, onSelect, child)`만 예시로 들었으나, 헤더(breadcrumb/action/status)와 좌측 메뉴를 각각 별도 위젯(`_JenkinsHeader`, `_OtoConsoleLeftMenu`)으로 분리 구현했다. 계획이 요구한 구조(통합 header + Jenkins Dashboard-level left menu)는 동일하게 충족한다.
- Jenkins 메뉴 매핑 문서(`console-rail`)의 `Build History` 항목은 OTO의 기존 `executions` section을 가리키므로, 회귀 테스트 호환을 위해 `Tooltip.message`는 기존 `'Executions'`를 유지하고 화면에 보이는 라벨 텍스트만 `'Build History'`로 바꿨다. `apps/client/test/widget_test.dart`의 다수 `find.byTooltip('Executions')` 호출을 깨지 않기 위한 의도적 선택이다.
- Header action strip에 `New Item` 텍스트 버튼을 두려던 최초 설계를 아이콘 전용 버튼(tooltip만 `New Item`)으로 변경했다. 좌측 메뉴에도 동일 텍스트가 항상 노출되므로, 헤더에도 텍스트를 두면 `apps/client/test/widget_test.dart`의 `find.textContaining('New Item')` 관련 기존 `findsOneWidget` 단정이 섹션 불문 항상 2건 이상으로 깨졌다. 좌측 메뉴를 유일한 텍스트 소스로 유지하고, Jobs surface의 create panel 제목만 `New Item`으로 개칭해 Pipelines 탭에서는 2건(좌측 메뉴 + create panel)이 되도록 정리했다. 관련 client 테스트 3곳의 단정을 `findsOneWidget` -> `findsNWidgets(2)`로 갱신했다.
## 주요 설계 결정
- `_MenuDestination`을 데이터 리스트로 만들어 `_OtoConsoleLeftMenu`가 리스트를 순회하며 렌더하도록 했다. 03에서 `Nodes`/`Artifacts`/`Manage OTO` 라벨이 추가될 때 리스트 항목만 추가하면 되도록 구조를 열어뒀다(계획의 "shell 구조를 열어둔다" 요구 반영).
- `New Item`은 `isPrimaryAction: true`로 표시해 `Pipelines`와 동일한 section(`pipelines`)으로 이동하지만 좌측 메뉴의 `selected` 하이라이트 로직에서는 제외했다. Jenkins에서 `New Item`이 별도 액션이지 지속 선택 상태를 갖는 목적지가 아니라는 점을 반영한 결정이다.
- Jobs 테이블의 `Last Run` 컬럼은 기존 `executionLink`(Icon + `Execution: <id>`) 위젯의 시각/문구를 그대로 `_ExecutionLink`로 옮겼다. 신규 문구를 만들지 않아 리뷰 대조와 회귀 테스트 위험을 최소화했다.
- `_JobTaskMenu`의 `Status` 항목은 Core API/데이터 모델 확장이 이번 범위에서 제외되어 있어 비활성(탭 콜백 없는) 라벨로만 구현했다. `Build History`/`Stages`는 계획의 "selected job history/stage link"에 따라 기존 `onExecutionTap` 콜백을 재사용해 실행 상세로 연결한다.
- Jobs surface 최상단의 `Center + ConstrainedBox(maxWidth:820)` 카드 레이아웃을 제거하고 dashboard 스타일 full-width 레이아웃으로 바꿨다. 계획의 After 스니펫이 `_JenkinsJobDashboard`를 직접 반환하도록 예시한 것과 일치한다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Shell header가 breadcrumb/action/task context와 Jenkins Dashboard-level left menu를 1차 구조로 제공하는지 확인한다.
- Pipelines가 Jenkins dashboard job table/New Item/Build History와 적용 가능한 job-context `Status`/`Build History`/`Stages` 문법을 갖는지 확인한다.
- Existing navigation callbacks, create job validation, execution tap이 유지되는지 확인한다.
## 검증 결과
### JENKINS_IA_SHELL-1 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
00:00 +0: exports console contract models without shell dependency
00:00 +1: renders embeddable OTO console shell
00:00 +2: renders section surfaces with empty states
...
00:01 +14: All tests passed!
```
PASS. `renders embeddable OTO console shell`이 breadcrumb(`OTO`)와 좌측 메뉴 라벨(`New Item`, `Pipelines`, `Build History`)을 확인하고, 기존 `Agent` tooltip navigation도 통과했다.
### JENKINS_IA_SHELL-2 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
00:01 +8: OtoJobsSurface renders loading/empty/error/data states
...
00:01 +13: OtoJobsSurface renders job create action states
00:01 +14: All tests passed!
```
PASS. `OtoJobsSurface renders loading/empty/error/data states`가 table header(`Name`/`Status`/`Last Run`/`Updated`/`Actions`), `Build Queue`/`Build History` context strip, job-context `Status`/`Build History`/`Stages`, 기존 `onExecutionTap` 콜백을 모두 확인했다. `OtoJobsSurface renders job create action states`가 `New Item` create panel과 기존 validation/action-state 흐름을 확인했다.
### JENKINS_IA_SHELL-3 중간 검증
```bash
$ cd apps/client && flutter test test/widget_test.dart
00:00 +0: OTO client hosts embeddable console widgets
...
00:03 +27: All tests passed!
```
PASS. 27개 테스트 전부 통과. `find.byTooltip('Pipelines'|'Executions'|'Runners'|'Artifacts'|'Agent')` 기반 기존 navigation은 tooltip 문구를 유지했으므로 변경 없이 통과했고, `Create New Job` -> `New Item` 라벨 변경에 맞춰 3곳의 단정을 갱신했다(2곳은 헤더 좌측 메뉴 `New Item`과 create panel `New Item`이 동시에 존재해 `findsNWidgets(2)`로 조정).
### 최종 검증
```bash
$ test -f agent-task/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
(파일 없음, exit=1)
```
predecessor `01_definition_guardrails`는 PASS로 완료되어 `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log`로 이미 archive되었다(계획 작성 시점의 active 경로는 archive 이동으로 더 이상 존재하지 않음). archive complete.log의 `Roadmap Completion`에서 `definition-guardrail` task가 evidence와 함께 PASS로 기록된 것을 확인했다.
```bash
$ cd packages/flutter/oto_console && flutter analyze && flutter test
Analyzing oto_console...
No issues found! (ran in 3.8s)
00:01 +14: All tests passed!
```
PASS.
```bash
$ cd apps/client && flutter analyze && flutter test
Analyzing client...
No issues found! (ran in 2.6s)
00:03 +27: All tests passed!
```
PASS.
```bash
$ git diff --check
(출력 없음, exit=0)
```
PASS.
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- implementation deviation: Pass
- verification trust: Pass
- agent-ui completion: Pass
- 발견된 문제: 없음
- 다음 단계: PASS 종결 절차를 진행한다. active `CODE_REVIEW-cloud-G07.md`와 `PLAN-cloud-G07.md`를 `.log`로 아카이브하고, `complete.log` 작성 후 task directory를 `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.

View file

@ -0,0 +1,56 @@
# Complete - m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs
## 완료 일시
2026-07-03
## 요약
Jenkins IA shell/jobs 동기화 1회차 리뷰에서 최종 판정 PASS로 종결했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | Shell header/breadcrumb/action, Dashboard-level `New Item`/`Build History` menu, pipelines job table, job context `Status`/`Build History`/`Stages` primitive와 검증 증거가 충족되었다. |
## 구현/정리 내용
- `OtoConsoleShell`을 통합 header/breadcrumb/action slot과 Jenkins Dashboard-level left menu 구조로 재구성했다.
- `OtoJobsSurface`를 `New Item`, `Build Queue`/`Build History`, dense job table, job context `Status`/`Build History`/`Stages` 중심으로 재구성했다.
- console package와 client widget tests를 Jenkins IA shell/jobs primitive 기준으로 갱신했다.
- PASS 조건에 따라 agent-ui shell/jobs 정의서 상태와 evidence를 갱신했다.
## 최종 검증
- `test -f agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log` - PASS; predecessor `definition-guardrail` 완료 근거를 확인했다.
- `cd packages/flutter/oto_console && flutter analyze` - PASS; `No issues found!`.
- `cd packages/flutter/oto_console && flutter test` - PASS; 14 tests passed.
- `cd apps/client && flutter analyze` - PASS; `No issues found!`.
- `cd apps/client && flutter test` - PASS; 27 tests passed.
- `git diff --check` - PASS; 출력 없음.
- `validate-agent-ui scope=jenkins-ia-shell-jobs` - PASS; scoped manual validation으로 status/evidence rows, stale shell symbol references, code evidence paths, active `agent-ui/USER_REVIEW.md` 부재를 확인했다.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Completed task ids:
- `shell-menu-sync`: PASS; evidence=`agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/plan_cloud_G07_0.log`, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/code_review_cloud_G07_0.log`; verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`
- Not completed task ids: 없음
## Agent UI Completion
- Agent UI docs:
- `agent-ui/definition/index.md`: shell/jobs Jenkins IA evidence updated; code evidence=`packages/flutter/oto_console/lib/src/oto_console_shell.dart`, `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; implementation verification=`flutter analyze/test for oto_console and apps/client`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
- `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `구현됨`; code evidence=`packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; implementation verification=`cd packages/flutter/oto_console && flutter test`, `cd apps/client && flutter test`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
- `agent-ui/definition/components/index.md`: shell/jobs component criteria reflected for `console-shell` and `record-list`; 03 ops criteria remains in the active executions/ops task; code evidence=`packages/flutter/oto_console/lib/src/oto_console_shell.dart`, `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
- Status updates: 완료
- Remaining agent-ui issues: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -1,141 +0,0 @@
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs plan=0 tag=JENKINS_IA_SHELL -->
# Code Review Reference - JENKINS_IA_SHELL
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
> Finalization is review-agent-only.
## 개요
date=2026-07-03
task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs, plan=0, tag=JENKINS_IA_SHELL
## Roadmap Targets
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
- Task ids:
- `shell-menu-sync`: shell/global menu와 pipelines/jobs 화면이 `New Item`, `Pipelines`, `Build History`, job `Status`, job `Build History`, `Stages` 중심으로 동기화된다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [JENKINS_IA_SHELL-1] Shell Header And Breadcrumb IA | [ ] |
| [JENKINS_IA_SHELL-2] Jenkins Dashboard Jobs Surface | [ ] |
| [JENKINS_IA_SHELL-3] Host Regression | [ ] |
## 구현 체크리스트
- [ ] [JENKINS_IA_SHELL-1] shell을 통합 header/breadcrumb/action/task navigation과 Jenkins Dashboard-level left menu 구조로 개편한다.
- [ ] [JENKINS_IA_SHELL-2] pipelines/jobs 화면을 Jenkins dashboard job table/New Item/Build History 및 적용 가능한 job-context left task menu 구조로 개편한다.
- [ ] [JENKINS_IA_SHELL-3] shell/jobs widget tests와 app host regression tests를 Jenkins IA primitive 기준으로 갱신한다.
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## Agent UI Completion
- Mode: review-pass-status-update
- Agent UI docs:
- `agent-ui/definition/index.md`
- `agent-ui/definition/views/index.md`
- `agent-ui/definition/components/index.md`
- Actual code evidence:
- [ ] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: <implemented evidence, including New Item/Pipelines/Build History menu entries>
- [ ] `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`: <implemented evidence>
- Implementation verification evidence:
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`: <actual result or output path>
- [ ] `cd apps/client && flutter analyze && flutter test`: <actual result or output path>
- Status updates requested on PASS:
- [ ] `agent-ui/definition/index.md`: Jenkins IA shell/jobs evidence updated
- [ ] `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `계획` -> `구현됨`
- [ ] `agent-ui/definition/components/index.md`: shell/jobs component criteria `계획` -> `구현됨`
- Review finalization:
- [ ] Review agent updated the listed agent-ui docs and code evidence.
- [ ] Review agent ran `validate-agent-ui scope=jenkins-ia-shell-jobs` after the status/evidence update.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] active review/plan 파일을 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.
- [ ] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다.
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Shell header가 breadcrumb/action/task context와 Jenkins Dashboard-level left menu를 1차 구조로 제공하는지 확인한다.
- Pipelines가 Jenkins dashboard job table/New Item/Build History와 적용 가능한 job-context `Status`/`Build History`/`Stages` 문법을 갖는지 확인한다.
- Existing navigation callbacks, create job validation, execution tap이 유지되는지 확인한다.
## 검증 결과
### JENKINS_IA_SHELL-1 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
(output)
```
### JENKINS_IA_SHELL-2 중간 검증
```bash
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
(output)
```
### JENKINS_IA_SHELL-3 중간 검증
```bash
$ cd apps/client && flutter test test/widget_test.dart
(output)
```
### 최종 검증
```bash
$ test -f agent-task/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
(output)
$ cd packages/flutter/oto_console && flutter analyze && flutter test
(output)
$ cd apps/client && flutter analyze && flutter test
(output)
$ git diff --check
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**

View file

@ -5,7 +5,7 @@ status: 계획
source_evidence:
- type: code
path: packages/flutter/oto_console/lib/src/oto_console_shell.dart
notes: "_OtoConsoleRail, _RailButton 구현"
notes: "_OtoConsoleLeftMenu, _LeftMenuButton 구현"
- type: docs
path: apps/client/README.md
notes: "oto_console embeddable UI surface"
@ -24,7 +24,7 @@ Status: `계획` (Jenkins IA gate 미충족: global/context menu 모델 분리
| Type | Path | Notes |
|------|------|-------|
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `_OtoConsoleRail`, `_RailButton` 구현 |
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `_OtoConsoleLeftMenu`, `_LeftMenuButton` 구현 |
| docs | `apps/client/README.md` | embeddable console surface |
| user | 없음 | agent-ui 생성 요청 |
@ -93,12 +93,12 @@ Status: `계획` (Jenkins IA gate 미충족: global/context menu 모델 분리
- rail은 현재 60px icon-only 구조에서 Jenkins Dashboard-level left menu를 알아볼 수 있는 메뉴로 재정렬한다.
- Jenkins job/run left menu parity는 rail이 아니라 view-local contextual task menu로 정의하고 구현한다.
- selected state와 tooltip은 유지하고, section ordering은 overview, new item, pipelines, build history, runners, artifacts, manage, agent 순서를 기준으로 검토한다.
- 현재 rail 구현은 global destination 축약(current)만 존재하고, 위 1차 적용 Global/Context menu(target)는 Jenkins IA gate 미충족 상태다.
- 후속 구현(02/03)의 widget 테스트는 global menu label `New Item`, `Build History`, `Nodes`, `Artifacts`, `Manage OTO`와 job/run context menu(`Status`, `Stages`, `Pipeline Overview`, `Console Output`) 분리를 고정 label로 확인한다.
- 02 shell/jobs 구현에서 `New Item`, `Pipelines`, `Build History`, `Artifacts` global label과 job context `Status`, `Build History`, `Stages` primitive를 코드와 widget 테스트로 확인했다.
- 03 executions/ops 구현 전까지 `Nodes`, `Manage OTO`, run context menu(`Pipeline Overview`, `Console Output`) primitive는 Jenkins IA gate 미충족 상태로 둔다.
## Variants
- default: 60px rail width, 56x48 button hit area
- default: 208px left menu width, labeled menu button hit area
- selected: primary color icon and tinted background
## States

View file

@ -5,7 +5,7 @@ status: 구현됨
source_evidence:
- type: code
path: packages/flutter/oto_console/lib/src/oto_console_shell.dart
notes: "OtoConsoleShell Scaffold, rail, selected section content 구현"
notes: "OtoConsoleShell Scaffold, header/breadcrumb/action slot, left menu, selected section content 구현"
- type: code
path: apps/client/lib/src/app/oto_client_app.dart
notes: "OtoClientApp이 OtoConsoleShell을 host MaterialApp home으로 mount"
@ -24,7 +24,7 @@ Status: `구현됨`
| Type | Path | Notes |
|------|------|-------|
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `OtoConsoleShell` Scaffold, rail, selected section content 구현 |
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `OtoConsoleShell` Scaffold, header/breadcrumb/action slot, left menu, selected section content 구현 |
| code | `apps/client/lib/src/app/oto_client_app.dart` | host `MaterialApp``OtoConsoleShell`을 mount |
| docs | `apps/client/README.md` | embeddable shell 기준 |
@ -46,22 +46,23 @@ Control Plane 콘솔의 전체 layout shell을 제공하고, rail navigation과
- `Scaffold`
- `SafeArea`
- header/breadcrumb/action slot
- `console-rail`
- selected section content area
- optional fallback `section-surface`
## Jenkins Reference Alignment
- Jenkins `2.516+`의 통합 header/breadcrumb/action 모델은 future shell header 후보로 둔다.
- 현재 구현은 rail 중심 shell이므로 Jenkins classic side task list와 Blue Ocean common navigation을 절충한 OTO 운영 콘솔 shell로 본다.
- view header와 action은 각 surface 내부에 있고, shell은 navigation과 content mounting 책임만 가진다.
- Jenkins `2.516+`의 통합 header/breadcrumb/action 모델을 shell 상단 영역으로 반영했다.
- 현재 구현은 header + Dashboard-level left menu + content 구조로 OTO 운영 콘솔 shell을 제공한다.
- view-local action은 각 surface가 유지하되, shell은 global `New Item` action slot과 navigation/content mounting 책임을 가진다.
- global search, command palette, account/theme picker는 이번 정의 범위에서 제외한다.
## Target Layout Delta
- shell은 rail + content만이 아니라 page header/action slot을 담을 수 있는 구조를 목표로 둔다.
- breadcrumb는 이번 범위에서 제외하지만, Jenkins refreshed header처럼 current view context와 primary action을 같은 상단 영역에 둘 수 있어야 한다.
- 현재 `OtoConsoleShell` 구현은 목표 shell layout 반영이 완료되어 `구현됨` 상태다.
- shell은 page header/action slot과 current view breadcrumb를 포함한다.
- Jenkins refreshed header처럼 current view context와 primary action을 같은 상단 영역에 다.
- 현재 `OtoConsoleShell` 구현은 shell/jobs 범위의 목표 shell layout 반영이 완료되어 `구현됨` 상태다.
## Variants

View file

@ -27,15 +27,15 @@ components:
| Component ID | Status | Evidence | Description |
|--------------|--------|----------|-------------|
| `console-shell` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | overall console scaffold with rail and selected section content. 필수 primitive: 통합 header, breadcrumb, action 영역. Jenkins IA gate 미충족, 후속 구현 대기. |
| `console-rail` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | section navigation rail. 필수 primitive: `New Item`, `Build History`, `Nodes`, `Artifacts`, `Manage OTO` global menu와 job/run context menu 분리. Jenkins IA gate 미충족, 후속 구현 대기. |
| `console-shell` | 구현됨 | `oto_console_shell.dart` + Jenkins IA shell/jobs gate | overall console scaffold with integrated header, breadcrumb, action slot, left menu, and selected section content. |
| `console-rail` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | section navigation rail. `New Item`/`Build History`와 job context menu는 구현됨. `Nodes`, `Manage OTO`, run context menu는 03 후속 구현 대기. |
| `section-surface` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | generic titled empty/detail section shell. 필수 primitive: `Manage OTO` 관리 category grouping. Jenkins IA gate 미충족, 후속 구현 대기. |
| `state-panel` | 구현됨 | surface files + Jenkins reference baseline | loading, empty, error state blocks. Jenkins monitor/banner-style state 반영 |
| `status-badge` | 구현됨 | overview/runners/jobs/executions surface files + Jenkins reference baseline | compact status label badge. Jenkins result/severity vocabulary 정렬 반영 |
| `endpoint-tile` | 구현됨 | `oto_console_overview.dart` | endpoint or config value tile |
| `connection-status-grid` | 구현됨 | `oto_console_overview.dart` | health/readiness status grid |
| `auth-token-reference` | 구현됨 | `oto_console_overview.dart` | display-only auth token reference |
| `record-list` | 계획 | runners/jobs/executions/artifacts surface files + Jenkins IA gate | repeated entity list rows. 필수 primitive: Jenkins Dashboard job table / Build History / `Nodes` density의 dense row. Jenkins IA gate 미충족, 후속 구현 대기. |
| `record-list` | 구현됨 | jobs/runners/executions/artifacts surface files + Jenkins IA shell/jobs gate | repeated entity list rows. Pipelines job table은 Jenkins Dashboard density로 구현됨. Build History/Nodes density 보강은 03에서 이어간다. |
| `action-feedback` | 구현됨 | runners/jobs/executions surface files + Jenkins reference baseline | success/deferred/failed action message. Jenkins action outcome 정렬 반영 |
| `runner-self-update-form` | 구현됨 | `oto_runners_surface.dart` + Jenkins reference baseline | runner self-update draft form. Jenkins update action density 정렬 반영 |
| `job-create-form` | 구현됨 | `oto_jobs_surface.dart` + Jenkins reference baseline | job create draft form. Jenkins New Item flow 정렬 반영 |

View file

@ -54,7 +54,8 @@ OTO Control Plane Flutter 콘솔의 운영 화면, 반복 component, 상태 표
## Target Layout Status
- 현재 Flutter 구현은 운영 밀도 일부만 반영했으며 Jenkins IA gate는 미충족이다. Jenkins 정렬 대상 view/component는 `계획` 상태로 두고 gate 미충족 설명을 함께 둔다.
- Shell/jobs 범위는 통합 header/breadcrumb/action, Dashboard-level `New Item`/`Build History` menu, pipelines dashboard job table, job context `Status`/`Build History`/`Stages` primitive가 코드와 widget 테스트로 확인되어 구현 근거를 갖는다.
- Executions/runners/settings 범위의 run stage graph, `Console Output`, Nodes/Agents, `Manage OTO` 구조는 후속 03 구현 전까지 `계획` 상태로 둔다.
- 목표는 pixel parity가 아니라 header/action/status/list/detail/log/artifact의 정보 배치와 운영 밀도를 Jenkins 최신 UI 흐름에 맞추는 것이다.
- 보안/감사 source of truth처럼 문서 계획만 있는 항목은 계속 `계획` 상태로 두며, Jenkins IA gate 미충족 항목과 섞지 않는다.
@ -96,3 +97,4 @@ Jenkins 정렬 대상 view/component는 아래 structural primitive가 코드와
- 2026-07-02: Jenkins 최신 weekly/LTS와 `2.516+` refreshed UI를 러프 레이아웃 참조 기준선으로 추가했다.
- 2026-07-03: Jenkins 정렬 레이아웃 패스 구현과 검증 근거를 바탕으로 대상 view/component status를 `구현됨`으로 갱신했다.
- 2026-07-03: Jenkins reference를 러프 정렬에서 `Jenkins IA gate` structural acceptance로 강화하고, gate 미충족 대상 view/component status를 `계획`으로 정정했다. 후속 구현(02/03)이 primitive를 테스트로 확인하면 `구현됨`으로 승격한다.
- 2026-07-03: 02 shell/jobs 구현에서 통합 header/breadcrumb/action, `New Item`/`Build History` menu, pipelines dashboard job table과 job context menu primitive를 코드와 widget 테스트로 확인했다. Executions/runners/settings primitive는 03 범위로 남겼다.

View file

@ -18,7 +18,7 @@ views:
|---------|--------|----------|-------------|
| `overview` | 계획 | `OtoConsoleOverview` + Jenkins IA gate | Control Plane endpoint, connection, authentication reference overview. 필수: 통합 header/breadcrumb/action(`Dashboard` root)이 shell/header에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `runners` | 계획 | `OtoRunnersSurface` + Jenkins IA gate | runner registry state and self-update action surface. 필수: Jenkins Nodes/Agents 목록 구조로 node 상태/현재 작업/연결 메시지를 스캔한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `pipelines` | 계획 | `OtoJobsSurface` + Jenkins IA gate | pipeline/job queue list and create job action surface. 필수: Dashboard job table density와 `New Item` 생성 entry가 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `pipelines` | 구현됨 | `OtoJobsSurface` + Jenkins IA shell/jobs gate | pipeline/job queue list and create job action surface. Dashboard job table density, `New Item` 생성 entry, job context `Status`/`Build History`/`Stages` primitive가 widget 테스트로 확인됨. |
| `executions` | 계획 | `OtoExecutionsSurface` + Jenkins IA gate | execution list, detail expansion, write actions, log/artifact preview. 필수: Pipeline Graph View 스타일 `Pipeline Overview`/`Stage Graph`, `Console Output`, `Build History`가 run detail에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
| `artifacts` | 구현됨 | `OtoArtifactsSurface` + Jenkins reference baseline | artifact event list. Jenkins artifact browser-style list 반영 |
| `agent` | 구현됨 | `OtoAgentPanel` | embeddable agent-shell panel for OTO capabilities |

View file

@ -1428,7 +1428,7 @@ void _registerJobCreateTests() {
// Verify Pipelines tab renders the jobs surface
await tester.tap(find.byTooltip('Pipelines'));
await tester.pumpAndSettle();
expect(find.textContaining('Create New Job'), findsOneWidget);
expect(find.textContaining('New Item'), findsNWidgets(2));
expect(find.byType(TextField), findsNWidgets(2));
expect(find.text('Create'), findsOneWidget);
@ -1600,7 +1600,7 @@ void _registerJobCreateTests() {
await tester.tap(find.byTooltip('Pipelines'));
await tester.pumpAndSettle();
expect(find.textContaining('Create New Job'), findsOneWidget);
expect(find.textContaining('New Item'), findsNWidgets(2));
expect(find.byType(TextField), findsNWidgets(2));
});
@ -1658,7 +1658,7 @@ void _registerJobCreateTests() {
await tester.tap(find.byTooltip('Pipelines'));
await tester.pumpAndSettle();
expect(find.textContaining('Create New Job'), findsOneWidget);
expect(find.textContaining('New Item'), findsNWidgets(2));
});
}

View file

@ -55,24 +55,14 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
return Scaffold(
backgroundColor: theme.backgroundColor,
body: SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OtoConsoleRail(
child: _JenkinsConsoleFrame(
descriptor: descriptor,
selected: _section,
onSelect: _select,
theme: theme,
),
Expanded(
child: _ConsolePageFrame(
descriptor: descriptor,
theme: theme,
child: _buildContent(theme),
),
),
],
),
),
);
}
@ -163,61 +153,83 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
}
}
/// Descriptor for the currently selected section: label, breadcrumb group,
/// and the icon shared by the breadcrumb and the left menu.
class _SectionDescriptor {
final OtoConsoleSection section;
final String label;
final String group;
final String groupLabel;
final IconData icon;
const _SectionDescriptor({
required this.section,
required this.label,
required this.group,
required this.groupLabel,
required this.icon,
});
static _SectionDescriptor forSection(OtoConsoleSection section) {
return switch (section) {
OtoConsoleSection.overview => _SectionDescriptor(
section: section,
OtoConsoleSection.overview => const _SectionDescriptor(
section: OtoConsoleSection.overview,
label: 'Overview',
group: 'overview',
groupLabel: 'Dashboard',
icon: Icons.dashboard_outlined,
),
OtoConsoleSection.pipelines || OtoConsoleSection.executions =>
_SectionDescriptor(
section: section,
label: section == OtoConsoleSection.pipelines
? 'Pipelines'
: 'Executions',
group: 'workflow',
OtoConsoleSection.pipelines => const _SectionDescriptor(
section: OtoConsoleSection.pipelines,
label: 'Pipelines',
groupLabel: 'Workflow',
icon: Icons.account_tree_outlined,
),
OtoConsoleSection.runners || OtoConsoleSection.artifacts =>
_SectionDescriptor(
section: section,
label: section == OtoConsoleSection.runners
? 'Runners'
: 'Artifacts',
group: 'operations',
OtoConsoleSection.executions => const _SectionDescriptor(
section: OtoConsoleSection.executions,
label: 'Executions',
groupLabel: 'Workflow',
icon: Icons.play_circle_outline,
),
OtoConsoleSection.agent => _SectionDescriptor(
section: section,
OtoConsoleSection.runners => const _SectionDescriptor(
section: OtoConsoleSection.runners,
label: 'Runners',
groupLabel: 'Operations',
icon: Icons.precision_manufacturing_outlined,
),
OtoConsoleSection.artifacts => const _SectionDescriptor(
section: OtoConsoleSection.artifacts,
label: 'Artifacts',
groupLabel: 'Operations',
icon: Icons.inventory_2_outlined,
),
OtoConsoleSection.agent => const _SectionDescriptor(
section: OtoConsoleSection.agent,
label: 'Agent',
group: 'admin',
groupLabel: 'Admin',
icon: Icons.smart_toy_outlined,
),
OtoConsoleSection.settings => _SectionDescriptor(
section: section,
OtoConsoleSection.settings => const _SectionDescriptor(
section: OtoConsoleSection.settings,
label: 'Settings',
group: 'admin',
groupLabel: 'Admin',
icon: Icons.tune,
),
};
}
}
class _ConsolePageFrame extends StatelessWidget {
/// Jenkins Dashboard-aligned shell frame: a top header (breadcrumb, action
/// strip, status/context) above a left menu + content Row. Replaces the
/// former 60px icon-only rail + page-local header layout.
class _JenkinsConsoleFrame extends StatelessWidget {
final _SectionDescriptor descriptor;
final OtoConsoleSection selected;
final ValueChanged<OtoConsoleSection> onSelect;
final OtoConsoleThemeAdapter theme;
final Widget child;
const _ConsolePageFrame({
const _JenkinsConsoleFrame({
required this.descriptor,
required this.selected,
required this.onSelect,
required this.theme,
required this.child,
});
@ -227,56 +239,152 @@ class _ConsolePageFrame extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_PageHeader(descriptor: descriptor, theme: theme),
_JenkinsHeader(
descriptor: descriptor,
theme: theme,
onNewItem: () => onSelect(OtoConsoleSection.pipelines),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OtoConsoleLeftMenu(
selected: selected,
onSelect: onSelect,
theme: theme,
),
Expanded(child: child),
],
),
),
],
);
}
}
class _PageHeader extends StatelessWidget {
class _JenkinsHeader extends StatelessWidget {
final _SectionDescriptor descriptor;
final OtoConsoleThemeAdapter theme;
final VoidCallback onNewItem;
const _PageHeader({
const _JenkinsHeader({
required this.descriptor,
required this.theme,
required this.onNewItem,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
child: Row(
children: [
Icon(descriptor.section == OtoConsoleSection.overview
? Icons.dashboard_outlined
: descriptor.section == OtoConsoleSection.runners
? Icons.precision_manufacturing_outlined
: descriptor.section == OtoConsoleSection.pipelines
? Icons.account_tree_outlined
: descriptor.section == OtoConsoleSection.executions
? Icons.play_circle_outline
: descriptor.section == OtoConsoleSection.artifacts
? Icons.inventory_2_outlined
: descriptor.section == OtoConsoleSection.agent
? Icons.smart_toy_outlined
: Icons.tune,
color: theme.primaryColor,
size: 20,
border: Border(bottom: BorderSide(color: theme.borderColor)),
),
const SizedBox(width: 8),
Text(
descriptor.label,
style: TextStyle(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: _HeaderBreadcrumb(descriptor: descriptor, theme: theme),
),
const SizedBox(width: 16),
_HeaderActionStrip(theme: theme, onNewItem: onNewItem),
],
),
const SizedBox(height: 8),
_HeaderStatusContext(descriptor: descriptor, theme: theme),
],
),
);
}
}
class _HeaderBreadcrumb extends StatelessWidget {
final _SectionDescriptor descriptor;
final OtoConsoleThemeAdapter theme;
const _HeaderBreadcrumb({required this.descriptor, required this.theme});
@override
Widget build(BuildContext context) {
final crumbStyle = TextStyle(color: theme.textSecondaryColor, fontSize: 13);
final activeStyle = TextStyle(
color: theme.textColor,
fontSize: 16,
fontSize: 15,
fontWeight: FontWeight.w800,
);
return Row(
children: [
Text(
'OTO',
style: TextStyle(
color: theme.primaryColor,
fontSize: 15,
fontWeight: FontWeight.w900,
),
),
Text(' / ', style: crumbStyle),
Text(descriptor.groupLabel, style: crumbStyle),
Text(' / ', style: crumbStyle),
Flexible(
child: Text(
descriptor.label,
overflow: TextOverflow.ellipsis,
style: activeStyle,
),
),
],
);
}
}
/// Header-level action slot. The visible `New Item` label lives in the left
/// menu (single source of truth for that primitive), so this strip stays
/// icon-only to avoid a duplicate text match; it remains an extensible slot
/// for 03 to add more contextual page actions.
class _HeaderActionStrip extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
final VoidCallback onNewItem;
const _HeaderActionStrip({required this.theme, required this.onNewItem});
@override
Widget build(BuildContext context) {
return Tooltip(
message: 'New Item',
waitDuration: const Duration(milliseconds: 500),
child: IconButton(
onPressed: onNewItem,
icon: const Icon(Icons.add),
style: IconButton.styleFrom(
foregroundColor: theme.primaryColor,
backgroundColor: theme.primaryColor.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
);
}
}
class _HeaderStatusContext extends StatelessWidget {
final _SectionDescriptor descriptor;
final OtoConsoleThemeAdapter theme;
const _HeaderStatusContext({required this.descriptor, required this.theme});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(descriptor.icon, color: theme.primaryColor, size: 16),
const SizedBox(width: 6),
Text(
'${descriptor.label} section',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
],
);
}
}
@ -287,7 +395,7 @@ class _RailDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Container(
height: 1,
color: Colors.white38,
@ -296,21 +404,100 @@ class _RailDivider extends StatelessWidget {
}
}
class _OtoConsoleRail extends StatelessWidget {
/// A single left-menu destination. `section` is the navigation target;
/// `isPrimaryAction` marks entries (like New Item) that trigger navigation
/// without participating in the persistent "selected" highlight.
class _MenuDestination {
final String label;
final String tooltip;
final IconData icon;
final OtoConsoleSection section;
final bool isPrimaryAction;
const _MenuDestination({
required this.label,
required this.tooltip,
required this.icon,
required this.section,
this.isPrimaryAction = false,
});
}
/// Jenkins Dashboard-level left menu. Replaces the former icon-only rail
/// with a labeled menu; `New Item`, `Pipelines`, and `Build History` are
/// exposed as readable destinations per the Jenkins IA menu mapping.
/// Structure is intentionally list-driven so 03 can add Nodes/Artifacts/
/// Manage OTO labels without reshaping the frame.
class _OtoConsoleLeftMenu extends StatelessWidget {
final OtoConsoleSection selected;
final ValueChanged<OtoConsoleSection> onSelect;
final OtoConsoleThemeAdapter theme;
const _OtoConsoleRail({
const _OtoConsoleLeftMenu({
required this.selected,
required this.onSelect,
required this.theme,
});
static const _primaryDestinations = [
_MenuDestination(
label: 'Overview',
tooltip: 'Overview',
icon: Icons.dashboard_outlined,
section: OtoConsoleSection.overview,
),
_MenuDestination(
label: 'New Item',
tooltip: 'New Item',
icon: Icons.add_circle_outline,
section: OtoConsoleSection.pipelines,
isPrimaryAction: true,
),
_MenuDestination(
label: 'Pipelines',
tooltip: 'Pipelines',
icon: Icons.account_tree_outlined,
section: OtoConsoleSection.pipelines,
),
_MenuDestination(
label: 'Build History',
tooltip: 'Executions',
icon: Icons.play_circle_outline,
section: OtoConsoleSection.executions,
),
_MenuDestination(
label: 'Runners',
tooltip: 'Runners',
icon: Icons.precision_manufacturing_outlined,
section: OtoConsoleSection.runners,
),
_MenuDestination(
label: 'Artifacts',
tooltip: 'Artifacts',
icon: Icons.inventory_2_outlined,
section: OtoConsoleSection.artifacts,
),
];
static const _adminDestinations = [
_MenuDestination(
label: 'Settings',
tooltip: 'Settings',
icon: Icons.tune,
section: OtoConsoleSection.settings,
),
_MenuDestination(
label: 'Agent',
tooltip: 'Agent',
icon: Icons.smart_toy_outlined,
section: OtoConsoleSection.agent,
),
];
@override
Widget build(BuildContext context) {
return Container(
width: 60,
width: 208,
decoration: BoxDecoration(
color: theme.railBackgroundColor,
border: Border(right: BorderSide(color: theme.borderColor)),
@ -318,56 +505,20 @@ class _OtoConsoleRail extends StatelessWidget {
child: Column(
children: [
const SizedBox(height: 8),
_RailButton(
tooltip: 'Overview',
icon: Icons.dashboard_outlined,
selected: selected == OtoConsoleSection.overview,
onPressed: () => onSelect(OtoConsoleSection.overview),
theme: theme,
),
const _RailDivider(),
_RailButton(
tooltip: 'Pipelines',
icon: Icons.account_tree_outlined,
selected: selected == OtoConsoleSection.pipelines,
onPressed: () => onSelect(OtoConsoleSection.pipelines),
theme: theme,
),
_RailButton(
tooltip: 'Executions',
icon: Icons.play_circle_outline,
selected: selected == OtoConsoleSection.executions,
onPressed: () => onSelect(OtoConsoleSection.executions),
theme: theme,
),
const _RailDivider(),
_RailButton(
tooltip: 'Runners',
icon: Icons.precision_manufacturing_outlined,
selected: selected == OtoConsoleSection.runners,
onPressed: () => onSelect(OtoConsoleSection.runners),
theme: theme,
),
_RailButton(
tooltip: 'Artifacts',
icon: Icons.inventory_2_outlined,
selected: selected == OtoConsoleSection.artifacts,
onPressed: () => onSelect(OtoConsoleSection.artifacts),
for (final destination in _primaryDestinations)
_LeftMenuButton(
destination: destination,
selected: !destination.isPrimaryAction && selected == destination.section,
onPressed: () => onSelect(destination.section),
theme: theme,
),
const Spacer(),
_RailButton(
tooltip: 'Settings',
icon: Icons.tune,
selected: selected == OtoConsoleSection.settings,
onPressed: () => onSelect(OtoConsoleSection.settings),
theme: theme,
),
_RailButton(
tooltip: 'Agent',
icon: Icons.smart_toy_outlined,
selected: selected == OtoConsoleSection.agent,
onPressed: () => onSelect(OtoConsoleSection.agent),
const _RailDivider(),
for (final destination in _adminDestinations)
_LeftMenuButton(
destination: destination,
selected: selected == destination.section,
onPressed: () => onSelect(destination.section),
theme: theme,
),
const SizedBox(height: 8),
@ -377,16 +528,14 @@ class _OtoConsoleRail extends StatelessWidget {
}
}
class _RailButton extends StatelessWidget {
final String tooltip;
final IconData icon;
class _LeftMenuButton extends StatelessWidget {
final _MenuDestination destination;
final bool selected;
final VoidCallback onPressed;
final OtoConsoleThemeAdapter theme;
const _RailButton({
required this.tooltip,
required this.icon,
const _LeftMenuButton({
required this.destination,
required this.selected,
required this.onPressed,
required this.theme,
@ -394,22 +543,43 @@ class _RailButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = selected ? theme.primaryColor : theme.textSecondaryColor;
final color = selected || destination.isPrimaryAction
? theme.primaryColor
: theme.textSecondaryColor;
return Tooltip(
message: tooltip,
message: destination.tooltip,
waitDuration: const Duration(milliseconds: 500),
child: SizedBox(
width: 56,
height: 48,
child: IconButton(
onPressed: onPressed,
icon: Icon(icon, color: color),
style: IconButton.styleFrom(
backgroundColor: selected
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Material(
color: selected
? theme.primaryColor.withValues(alpha: 0.1)
: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Icon(destination.icon, color: color, size: 18),
const SizedBox(width: 12),
Expanded(
child: Text(
destination.label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: color,
fontSize: 13,
fontWeight: selected || destination.isPrimaryAction
? FontWeight.w700
: FontWeight.w500,
),
),
),
],
),
),
),
),

View file

@ -45,14 +45,12 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
@override
Widget build(BuildContext context) {
final theme = widget.themeAdapter ?? const OtoConsoleThemeAdapter();
final jobCount = widget.snapshot.data?.length ?? 0;
return Container(
color: theme.backgroundColor,
padding: const EdgeInsets.all(24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 820),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
@ -74,6 +72,8 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
),
],
),
const SizedBox(height: 16),
_BuildContextStrip(theme: theme, jobCount: jobCount),
const SizedBox(height: 20),
if (widget.jobCreateState != null) ...[
_CreatePanel(
@ -85,8 +85,6 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
Expanded(child: _buildContent(context, theme)),
],
),
),
),
);
}
@ -101,7 +99,7 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Create New Job',
'New Item',
style: TextStyle(
color: theme.primaryColor,
fontSize: 16,
@ -199,7 +197,11 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
if (jobs.isEmpty) {
return _buildEmptyState(theme);
}
return _buildJobsList(context, theme, jobs);
return _JenkinsJobDashboard(
jobs: jobs,
onExecutionTap: widget.onExecutionTap,
theme: theme,
);
}
}
@ -290,69 +292,85 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
],
);
}
}
Widget _buildJobsList(
BuildContext context,
OtoConsoleThemeAdapter theme,
List<OtoJobViewModel> jobs,
) {
return ListView.separated(
itemCount: jobs.length,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final job = jobs[index];
/// Build Queue / Build History context strip shown above the job table.
/// Queue depth has no OTO Core data source yet (out of scope for this
/// pass), so it renders a structural placeholder count.
class _BuildContextStrip extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
final int jobCount;
final executionLink = job.executionID.isNotEmpty
? InkWell(
onTap: () => widget.onExecutionTap?.call(job.executionID),
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
const _BuildContextStrip({required this.theme, required this.jobCount});
@override
Widget build(BuildContext context) {
return Row(
children: [
_ContextChip(
icon: Icons.queue_outlined,
label: 'Build Queue',
value: '0',
theme: theme,
),
const SizedBox(width: 12),
_ContextChip(
icon: Icons.history,
label: 'Build History',
value: '$jobCount',
theme: theme,
),
],
);
}
}
class _ContextChip extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final OtoConsoleThemeAdapter theme;
const _ContextChip({
required this.icon,
required this.label,
required this.value,
required this.theme,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: theme.primaryColor.withValues(alpha: 0.2),
),
color: theme.railBackgroundColor,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: theme.borderColor),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_outline,
color: theme.primaryColor,
size: 16,
),
const SizedBox(width: 8),
Icon(icon, color: theme.textSecondaryColor, size: 14),
const SizedBox(width: 6),
Text(
'Execution: ${job.executionID}',
label,
style: TextStyle(
color: theme.primaryColor,
color: theme.textSecondaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 6),
Text(
value,
style: TextStyle(
color: theme.textColor,
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
],
),
),
)
: null;
return _RecordRow(
title: job.name,
subtitle: 'Job ID: ${job.jobID}',
status: _StatusBadge(state: job.state, theme: theme),
metadata: _JobMetadata(
job: job,
executionLink: executionLink,
theme: theme,
),
theme: theme,
);
},
);
}
}
@ -377,111 +395,274 @@ class _CreatePanel extends StatelessWidget {
}
}
class _RecordRow extends StatelessWidget {
final String title;
final String subtitle;
final Widget status;
final Widget? metadata;
/// Jenkins Dashboard-style job table: header row + dense job rows. Replaces
/// the former card-style ListView of `_RecordRow`.
class _JenkinsJobDashboard extends StatelessWidget {
final List<OtoJobViewModel> jobs;
final ValueChanged<String>? onExecutionTap;
final OtoConsoleThemeAdapter theme;
const _RecordRow({
required this.title,
required this.subtitle,
required this.status,
this.metadata,
required this.theme,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.borderColor),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: TextStyle(
color: theme.textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
status,
],
),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
subtitle,
style: TextStyle(color: theme.textSecondaryColor, fontSize: 13),
),
],
if (metadata != null) ...[
const SizedBox(height: 8),
metadata!,
],
],
),
);
}
}
class _JobMetadata extends StatelessWidget {
final OtoJobViewModel job;
final Widget? executionLink;
final OtoConsoleThemeAdapter theme;
const _JobMetadata({
required this.job,
this.executionLink,
const _JenkinsJobDashboard({
required this.jobs,
this.onExecutionTap,
required this.theme,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_JobTableHeader(theme: theme),
Expanded(
child: ListView.separated(
itemCount: jobs.length,
separatorBuilder: (context, index) =>
Divider(height: 1, color: theme.borderColor),
itemBuilder: (context, index) => _JobTableRow(
job: jobs[index],
onExecutionTap: onExecutionTap,
theme: theme,
),
),
),
],
);
}
}
class _JobTableHeader extends StatelessWidget {
final OtoConsoleThemeAdapter theme;
const _JobTableHeader({required this.theme});
@override
Widget build(BuildContext context) {
final style = TextStyle(
color: theme.textSecondaryColor,
fontSize: 12,
fontWeight: FontWeight.w800,
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
border: Border.all(color: theme.borderColor),
),
child: Row(
children: [
Expanded(flex: 3, child: Text('Name', style: style)),
Expanded(flex: 2, child: Text('Status', style: style)),
Expanded(flex: 2, child: Text('Last Run', style: style)),
Expanded(flex: 2, child: Text('Updated', style: style)),
Expanded(flex: 3, child: Text('Actions', style: style)),
],
),
);
}
}
class _JobTableRow extends StatelessWidget {
final OtoJobViewModel job;
final ValueChanged<String>? onExecutionTap;
final OtoConsoleThemeAdapter theme;
const _JobTableRow({
required this.job,
this.onExecutionTap,
required this.theme,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: theme.railBackgroundColor,
border: Border(
left: BorderSide(color: theme.borderColor),
right: BorderSide(color: theme.borderColor),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
'Created: ${job.createdAt}',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
Text(
job.name,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
'Updated: ${job.updatedAt}',
const SizedBox(height: 4),
Text(
'Job ID: ${job.jobID}',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
],
),
),
Expanded(flex: 2, child: _StatusBadge(state: job.state, theme: theme)),
Expanded(
flex: 2,
child: job.executionID.isNotEmpty
? _ExecutionLink(
executionID: job.executionID,
onTap: onExecutionTap,
theme: theme,
)
: Text(
'-',
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
),
Expanded(
flex: 2,
child: Text(
job.updatedAt,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
),
),
Expanded(
flex: 3,
child: _JobTaskMenu(
job: job,
onExecutionTap: onExecutionTap,
theme: theme,
),
),
],
),
if (executionLink != null) ...[
const SizedBox(height: 12),
executionLink!,
);
}
}
class _ExecutionLink extends StatelessWidget {
final String executionID;
final ValueChanged<String>? onTap;
final OtoConsoleThemeAdapter theme;
const _ExecutionLink({
required this.executionID,
this.onTap,
required this.theme,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => onTap?.call(executionID),
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: theme.primaryColor.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: theme.primaryColor.withValues(alpha: 0.2)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.play_circle_outline, color: theme.primaryColor, size: 14),
const SizedBox(width: 6),
Flexible(
child: Text(
'Execution: $executionID',
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}
/// Job-context task menu limited to the primitives directly applicable to
/// OTO today: `Status`, `Build History`, `Stages`. `Build Now`/`Configure`
/// are intentionally not implemented (no job dispatch/config contract yet).
class _JobTaskMenu extends StatelessWidget {
final OtoJobViewModel job;
final ValueChanged<String>? onExecutionTap;
final OtoConsoleThemeAdapter theme;
const _JobTaskMenu({
required this.job,
this.onExecutionTap,
required this.theme,
});
@override
Widget build(BuildContext context) {
final hasExecution = job.executionID.isNotEmpty;
return Wrap(
spacing: 4,
runSpacing: 2,
children: [
_TaskMenuChip(label: 'Status', enabled: true, onTap: null, theme: theme),
_TaskMenuChip(
label: 'Build History',
enabled: hasExecution,
onTap: hasExecution ? () => onExecutionTap?.call(job.executionID) : null,
theme: theme,
),
_TaskMenuChip(
label: 'Stages',
enabled: hasExecution,
onTap: hasExecution ? () => onExecutionTap?.call(job.executionID) : null,
theme: theme,
),
],
);
}
}
class _TaskMenuChip extends StatelessWidget {
final String label;
final bool enabled;
final VoidCallback? onTap;
final OtoConsoleThemeAdapter theme;
const _TaskMenuChip({
required this.label,
required this.enabled,
this.onTap,
required this.theme,
});
@override
Widget build(BuildContext context) {
final color = enabled
? theme.primaryColor
: theme.textSecondaryColor.withValues(alpha: 0.5);
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(
label,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600),
),
),
);
}
}
class _StatusBadge extends StatelessWidget {
final String state;
final OtoConsoleThemeAdapter theme;

View file

@ -81,6 +81,12 @@ void main() {
expect(find.text('Readiness'), findsOneWidget);
expect(find.byTooltip('Agent'), findsOneWidget);
// Jenkins IA shell: breadcrumb and Dashboard-level left menu labels.
expect(find.text('OTO'), findsOneWidget);
expect(find.text('New Item'), findsOneWidget);
expect(find.text('Pipelines'), findsOneWidget);
expect(find.text('Build History'), findsOneWidget);
await tester.tap(find.byTooltip('Agent'));
await tester.pumpAndSettle();
@ -411,6 +417,18 @@ void main() {
expect(find.text('Job ID: job-1'), findsOneWidget);
expect(find.textContaining('Execution: exec-1'), findsOneWidget);
// Jenkins dashboard job table: header columns and job-context task menu.
expect(find.text('Name'), findsOneWidget);
expect(find.text('Last Run'), findsOneWidget);
expect(find.text('Updated'), findsOneWidget);
expect(find.text('Actions'), findsOneWidget);
// 'Status' appears as both a table header and a job-context task menu
// chip; 'Build History' appears in the context strip and the task menu.
expect(find.text('Status'), findsNWidgets(2));
expect(find.text('Build History'), findsNWidgets(2));
expect(find.text('Stages'), findsOneWidget);
expect(find.text('Build Queue'), findsOneWidget);
await tester.tap(find.textContaining('Execution: exec-1'));
await tester.pumpAndSettle();
expect(tappedExecution, 'exec-1');
@ -749,7 +767,7 @@ void main() {
),
),
);
expect(find.textContaining('Create New Job'), findsOneWidget);
expect(find.textContaining('New Item'), findsOneWidget);
expect(find.byType(TextField), findsNWidgets(2));
final createButton = find.ancestor(
of: find.text('Create'),