<!-- task=m-agent-readable-repository-refactor/07_client_runtime_panel plan=1 tag=REVIEW_REFACTOR_CLIENT -->

# Flutter runtime panel file/state 보완 계획

## 이 파일을 읽는 구현 에이전트에게

아래 두 Required 항목만 구현하고 지정 검증을 그대로 실행한다. 실제 구현 내용과 stdout/stderr를 active `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션에 채운 뒤 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 구현을 차단할 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 대상과 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`·로그·`complete.log`를 직접 만들지 않는다. 환경/secret/서비스 차단, 일반 범위 조정, 재실행으로 채울 수 있는 검증 증거 공백은 사용자 리뷰 사유가 아니다.

## 배경

첫 구현은 `build`와 장기 section builder를 분해했지만 모든 widget을 같은 파일에 추가해 `runtime_panel.dart`를 693 LOC warning에서 810 LOC `split_review`로 악화시켰다. 또한 nullable operations response를 빈 목록으로 축약해 loaded-empty와 not-loaded 상태가 섞였다. 화면 문구·layout·repository 계약은 유지하면서 source 책임과 상태 의미를 함께 바로잡는다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청`에 기록하며, 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 따른다. 구현 중 직접 사용자 prompt는 금지하고 실제 `USER_REVIEW.md` 생성 여부 판단과 파일 작성은 code-review가 소유한다.

## Archive Evidence Snapshot

- Task: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel`
- Prior plan: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel/plan_local_G06_0.log`
- Prior review: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel/code_review_local_G06_0.log`
- Verdict: `FAIL`
- Findings: Required 2, Suggested 0, Nit 0
  - `runtime_panel.dart`가 810 LOC `split_review`로 악화됐고 baseline이 이를 허용했다.
  - nullable operations response가 빈 목록으로 축약돼 loaded-empty command pending 상태가 loading으로 바뀌며 관련 empty/error 전이 테스트가 없다.
- Affected files: `apps/client/lib/widgets/runtime_panel.dart`, `apps/client/test/runtime_panel_test.dart`, `apps/client/test/support/client_test_harness.dart`, `scripts/readability_read_sets.json`, `scripts/readability_baseline.json`
- Verification evidence:
  - `flutter test test/runtime_panel_test.dart`: PASS, 3 tests.
  - `make client-test`: PASS, 38 tests.
  - `flutter analyze`: 기존 caller lint 2건을 review agent가 중괄호로 수정한 뒤 PASS.
  - `make readability-audit`: exit 0이나 `runtime_panel.dart file_loc=810 level=split_review`를 확인해 완료 조건 미충족.
  - `git diff --check`: PASS.
- Reviewer repair: `apps/client/lib/client_home_page.dart`의 기존 단일문 `if` 두 곳에 중괄호를 추가했으며 추가 수정 대상이 아니다.
- Roadmap carryover: 원 계획에 `Roadmap Targets`가 없으므로 이 follow-up PASS도 Milestone Task id를 직접 체크하지 않는다.
- 추가 상세가 꼭 필요할 때만 위 `plan_local_G06_0.log`와 `code_review_local_G06_0.log`를 좁게 읽는다.

## 분석 결과

### 읽은 파일

- `apps/client/lib/widgets/runtime_panel.dart`
- `apps/client/lib/client_home_page.dart`
- `apps/client/lib/control_plane_status_client.dart`
- `apps/client/lib/control_plane_status_dto.dart`
- `apps/client/lib/control_plane_status_repository.dart`
- `apps/client/test/runtime_panel_test.dart`
- `apps/client/test/support/client_test_harness.dart`
- `apps/client/pubspec.yaml`
- `scripts/readability_read_sets.json`
- `scripts/readability_baseline.json`의 runtime panel/read-set 항목
- `build/readability-audit.json`의 runtime panel/read-set 결과
- `agent-test/local/rules.md`
- `agent-test/local/client-smoke.md`

### SDD 기준

- 선택 Milestone은 `SDD: 불필요`다. 외부 API/proto/config schema나 runtime 책임 경계를 바꾸지 않는 동일 client domain의 동작 보존형 책임 분리이기 때문이다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 `agent-test/local/client-smoke.md`를 읽었다.
- runner는 현재 Codex local runner, repo root는 `/config/workspace/iop`, branch는 `main`, 분석 시 HEAD는 `57f0eaa`, worktree는 병렬 subtask 변경을 포함한 dirty 상태다.
- Flutter `/sdk/flutter/bin/flutter` 3.41.5, Dart 3.11.3을 확인했다.
- 필수 검증은 `make client-test`, 보조 검증은 `flutter analyze`, 범위 widget smoke는 `flutter test test/runtime_panel_test.dart`다.
- 외부 endpoint, Docker, emulator/device, credential은 필요하지 않는다. `<확인 필요>` 값과 test-rule 유지보수 필요는 없다.

### 테스트 커버리지 공백

- 기존 test는 populated history, Edge 전환 중 not-loaded spinner, command 성공/실패 banner, command 입력 gate를 검증한다.
- loaded-empty response에서 command future가 pending인 동안 empty view를 유지하는 상태는 검증하지 않는다.
- operations fetch empty response와 exception error view를 독립적으로 검증하지 않는다.
- 현재 `main`이 235 LOC function `split_review`이므로 새 case를 단순 append하지 않고 scenario 등록 helper로 나눠야 한다.

### 심볼 참조

- 공개 `RuntimePanel` call site는 `apps/client/lib/client_home_page.dart:186` 한 곳이다.
- 기존 `_DomainAgentsGrid`, `_OperationsHistoryList`와 card/empty/loading/error widget은 `runtime_panel.dart` 내부 private symbol뿐이다.
- repository/DTO public symbol과 `RuntimePanel` 생성자 rename은 하지 않는다.

### 분할 판단

- 기존 split subtask `07_client_runtime_panel` 안의 follow-up 단일 plan을 유지한다.
- source section 분리와 state regression test는 같은 Flutter widget 책임이고 같은 targeted/full client 검증을 사용하므로 별도 subtask로 나누면 하나의 상태 계약을 두 번 조정하게 된다.
- public API foundation, 다수 call-site rollout, 별도 도메인, 외부 UI runner 경계가 없고 이 subtask 이름에도 predecessor dependency가 없다.

### 범위 결정 근거

- layout, 문구, 색상, command operation, controller/repository/DTO 계약은 변경하지 않는다.
- 새 generic widget package나 `utils`를 만들지 않는다.
- `client_home_page.dart`의 review-agent lint 수정 외 추가 caller 변경을 하지 않는다.
- 다른 readability baseline/read-set 항목과 다른 active subtask 변경을 재생성·정리하지 않는다.
- agent-ui 정의 변경이 아닌 same-feature refactor이므로 Agent UI Completion을 두지 않는다.

### 빌드 등급

- `local-G06`: 한 Flutter feature의 source/test/baseline 보완이며 동작 기대와 결정적 local 검증이 명확하다.

## 구현 체크리스트

- [ ] Domain Agents와 Operations History 표시 widget을 응집된 feature-local source로 분리하고 `runtime_panel.dart`의 `file_loc` split-review를 제거하며 `client-ui-readability` total을 1969 LOC 이하로 유지한다.
- [ ] nullable operations response의 not-loaded/loaded-empty 의미를 보존하고 loading/empty/error/populated 및 loaded-empty command pending widget tests를 독립 scenario로 검증한다.
- [ ] targeted test, client 전체 test/analyze, deterministic readability 검사와 ratchet, `git diff --check`를 모두 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REFACTOR_CLIENT-1] Runtime panel source split-review 제거

문제: `apps/client/lib/widgets/runtime_panel.dart:511-807`에 상태 비소유 Domain Agents/Operations History widget을 모두 추가해 file LOC가 810이 됐고, `scripts/readability_baseline.json:34-38`이 이를 `split_review`로 허용한다.

해결 방법:

Before (`apps/client/lib/widgets/runtime_panel.dart:511`):

```dart
class _DomainAgentsGrid extends StatelessWidget {
  // Domain Agents와 Operations History의 모든 section/card/state widget이
  // runtime_panel.dart 끝까지 같은 source에 남아 있다.
}
```

After:

```dart
import 'runtime_panel_sections.dart';

RuntimePanelDomainAgentsSection(edge: selectedEdge)
RuntimePanelOperationsHistorySection(
  isLoading: _isLoading,
  error: _error,
  response: _operations,
)
```

`runtime_panel_sections.dart`에는 두 feature-local section boundary와 그 내부 private card/empty/loading/error widget만 둔다. `runtime_panel.dart`는 state, command/dialog interaction, top-level composition을 소유한다. `scripts/readability_read_sets.json`의 `client-ui-readability`에 새 source를 추가하고, `/tmp` audit 결과로 runtime panel 관련 baseline 항목만 낮아진 현재 값에 맞춘다. 새 `split_review` 또는 read-set total 증가는 baseline에 추가하지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/widgets/runtime_panel.dart`: section rendering 구현을 제거하고 feature-local boundary를 import/조립한다.
- [ ] `apps/client/lib/widgets/runtime_panel_sections.dart`: Domain Agents/Operations History section과 내부 상태 비소유 widget을 둔다.
- [ ] `scripts/readability_read_sets.json`: `client-ui-readability`에 새 source를 포함한다.
- [ ] `scripts/readability_baseline.json`: runtime panel file violation을 제거/하향하고 read-set total을 1969 이하 실제 값으로 동기화한다.

테스트 작성: 구조 변경 자체의 새 snapshot test는 작성하지 않는다. 기존 렌더링 assertion과 REVIEW_REFACTOR_CLIENT-2의 상태별 regression test로 동작 보존을 검증한다.

중간 검증:

```bash
python3 scripts/readability_audit.py --input-mode worktree --output /tmp/iop-client-readability.json
jq -e '[.violations[] | select(.path == "apps/client/lib/widgets/runtime_panel.dart" and (.level == "split_review" or .level == "exception"))] | length == 0' /tmp/iop-client-readability.json
jq -e '[.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc] | length == 1 and .[0] <= 1969' /tmp/iop-client-readability.json
```

기대 결과: 두 `jq -e`가 exit 0이고 새 source가 read set에 포함된 상태에서도 total이 1969 이하이다.

### [REVIEW_REFACTOR_CLIENT-2] Operations history 상태 의미와 regression tests 복구

문제: `apps/client/lib/widgets/runtime_panel.dart:500-504`가 `_operations == null`을 `[]`로 바꾸고 `:664`가 `isLoading && operations.isEmpty`를 loading으로 처리한다. loaded-empty 상태에서 command future가 pending이면 기존 empty view 대신 spinner가 나타난다.

해결 방법:

Before (`apps/client/lib/widgets/runtime_panel.dart:500`):

```dart
_OperationsHistoryList(
  isLoading: _isLoading,
  error: _error,
  operations: _operations?.operations ?? [],
)
```

After:

```dart
RuntimePanelOperationsHistorySection(
  isLoading: _isLoading,
  error: _error,
  response: _operations,
)

if (isLoading && response == null) {
  return const _OperationsHistoryLoading();
}
final operations = response?.operations ?? const <EdgeCommandRecordView>[];
```

`runtime_panel_test.dart`의 긴 `main`은 scenario 등록 helper와 공통 pump helper로 나눠 새 function split-review를 만들지 않는다. test fake에는 edge-a operations override, fetch error, deferred command response처럼 필요한 결정적 제어점만 추가한다.

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/widgets/runtime_panel.dart`: nullable response를 section boundary까지 전달한다.
- [ ] `apps/client/lib/widgets/runtime_panel_sections.dart`: not-loaded/loaded-empty/error/populated 판정 순서를 기존 의미와 일치시킨다.
- [ ] `apps/client/test/support/client_test_harness.dart`: empty/error/deferred command를 제어하는 최소 fake hook을 추가한다.
- [ ] `apps/client/test/runtime_panel_test.dart`: 기존 scenario를 작은 등록 helper로 정리하고 아래 regression을 추가한다.

테스트 작성:

- `apps/client/test/runtime_panel_test.dart` — `RuntimePanel keeps loaded empty history visible while a command is pending`: empty response 로드 후 deferred `health.check` 중 spinner가 없고 `No operation executions recorded.`가 유지되는지 검증한다.
- `apps/client/test/runtime_panel_test.dart` — `RuntimePanel renders operations empty and fetch error states`: empty 문구와 repository exception의 `Error:` 문구를 각각 검증한다.
- 기존 Edge switch not-loaded spinner와 populated history assertion은 유지한다.

중간 검증:

```bash
cd apps/client && flutter test test/runtime_panel_test.dart
```

기대 결과: 모든 runtime panel widget test가 exit 0이고 새 상태 regression case가 실행된다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/widgets/runtime_panel.dart` | REVIEW_REFACTOR_CLIENT-1, REVIEW_REFACTOR_CLIENT-2 |
| `apps/client/lib/widgets/runtime_panel_sections.dart` | REVIEW_REFACTOR_CLIENT-1, REVIEW_REFACTOR_CLIENT-2 |
| `apps/client/test/runtime_panel_test.dart` | REVIEW_REFACTOR_CLIENT-2 |
| `apps/client/test/support/client_test_harness.dart` | REVIEW_REFACTOR_CLIENT-2 |
| `scripts/readability_read_sets.json` | REVIEW_REFACTOR_CLIENT-1 |
| `scripts/readability_baseline.json` | REVIEW_REFACTOR_CLIENT-1 |

## 최종 검증

```bash
(cd apps/client && flutter test test/runtime_panel_test.dart)
make client-test
(cd apps/client && flutter analyze)
python3 scripts/readability_audit.py --input-mode worktree --output /tmp/iop-client-readability.json
jq -e '[.violations[] | select(.path == "apps/client/lib/widgets/runtime_panel.dart" and (.level == "split_review" or .level == "exception"))] | length == 0' /tmp/iop-client-readability.json
jq -e '[.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc] | length == 1 and .[0] <= 1969' /tmp/iop-client-readability.json
jq -e '.tasks[] | select(.task_id == "client-ui-readability") | (.files | index("apps/client/lib/widgets/runtime_panel_sections.dart") != null)' scripts/readability_read_sets.json
make readability-audit
git diff --check
```

모든 명령이 exit 0이어야 한다. `/tmp` report에는 `runtime_panel.dart`의 `split_review`/`exception`이 없어야 하고 `client-ui-readability` total은 1969 이하이며, ratchet은 새/증가 위반 없이 통과해야 한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
