<!-- task=m-logcat-console/02+01_console_stream plan=0 tag=LOGCAT_STREAM -->

# LOGCAT_STREAM 계획

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것은 필수입니다. 구현 후 검증을 실행하고 실제 내용과 출력을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용입니다. 구현이 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 기록하고 중단하세요. 직접 질문, 채팅 선택지, `request_user_input`, `USER_REVIEW.md` 작성, archive, `complete.log` 작성은 금지입니다. 후속 에이전트가 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아닙니다.

## 배경

Console 화면은 현재 샘플 로그만 렌더링하고 실제 device 선택이나 ADB stream과 연결되지 않습니다. Devices 화면의 logcat 버튼도 no-op이라 사용자가 설치 후 바로 로그를 볼 수 없습니다. `01_logcat_process`의 session API 위에 shell/device 선택과 console stream lifecycle을 얹어 `stream`, `lifecycle` Task를 완료 가능한 단위로 만듭니다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록합니다. 구현 에이전트는 사용자에게 직접 질문하지 않으며, code-review가 blocker 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
- Task ids:
  - `stream`: 선택 device의 `adb logcat` stream을 시작하고 화면에 append한다.
  - `lifecycle`: 화면 이탈 또는 device 변경 시 logcat process를 정리한다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/usable-mvp/PHASE.md`
- `agent-roadmap/phase/usable-mvp/milestones/logcat-console.md`
- `agent-ops/rules/project/domain/device-console/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/device-console-smoke.md`
- `lib/src/services/adb_service.dart`
- `lib/src/features/console/console_page.dart`
- `lib/src/features/app_shell.dart`
- `lib/src/app.dart`
- `lib/src/features/devices/devices_page.dart`
- `lib/src/models/adb_device.dart`
- `pubspec.yaml`
- `test/adb_service_test.dart`
- `test/widget_test.dart`
- `test/console_page_test.dart`

### 테스트 환경 규칙

- test_env: `local`
- `agent-test/local/rules.md` 존재 및 읽음.
- 매칭 profile: `agent-test/local/device-console-smoke.md`
- 적용 명령: remote runner 기준 `flutter analyze`, `flutter test`; ADB process stream은 dispose 없이 남지 않아야 함.
- 이 plan은 fake stream/session으로 widget lifecycle을 검증하고, 실제 ADB runtime은 보조 smoke 후보로 남긴다.

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

- `ConsolePage`가 live stream을 append하는 test 없음. 새 widget test 필요.
- 화면 이탈/device 변경 시 session stop이 호출되는 test 없음. fake session stop counter로 검증해야 한다.
- 실제 `adb logcat` runtime smoke는 자동 unit test로 대체할 수 없다. remote runner에서 `adb devices -l`은 보조 확인으로 기록한다.

### 심볼 참조

- renamed/removed symbol: none.
- `ConsolePage` call site: `lib/src/features/app_shell.dart:201-205`.
- device logcat button: `lib/src/features/devices/devices_page.dart:368-372`.
- `AdbService` default wiring: `lib/src/app.dart:49`, `lib/src/app.dart:150-158`.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 공유 task group: `m-logcat-console`
- 이 subtask는 `02+01_console_stream`이며 predecessor `01`은 `agent-task/m-logcat-console/01_logcat_process/complete.log` 또는 archive matching `complete.log`가 필요하다.
- 현재 확인 결과 predecessor complete.log는 `agent-task/archive/2026/06/m-logcat-console/01_logcat_process/complete.log`에 있다. `01_logcat_process` PASS 완료 조건은 충족됐다.
- `03+02_auto_scroll`은 이 plan의 live console UI에 의존한다.

### 범위 결정 근거

- auto-scroll의 pause-aware scroll positioning은 제외하고 `03+02_auto_scroll`에서 처리한다.
- package/tag/text filter, level filter, export/clipboard, clear button은 `filter` 에픽 소속이므로 제외한다.
- macOS entitlements와 bundled adb 배포는 변경하지 않는다.

### 빌드 등급

- build/review: `local-G06`. UI wiring과 lifecycle test가 필요하지만, fake session과 widget tests로 범위를 제어할 수 있다.

## 구현 체크리스트

- [ ] `AppSokApp`에서 logcat session starter를 `AppSokShell`로 주입한다.
- [ ] `DevicesPage` logcat 버튼이 ready device를 console 선택 상태로 넘기고 콘솔 탭으로 이동하게 한다.
- [ ] `ConsolePage`가 선택 device의 logcat stream을 시작하고 화면에 append하며 dispose/device 변경 시 session을 정리한다.
- [ ] `flutter test test/widget_test.dart test/console_page_test.dart`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- Directory dependency: `02+01_console_stream` -> `01_logcat_process`
- 구현 전 `agent-task/m-logcat-console/01_logcat_process/complete.log` 또는 archive `agent-task/archive/*/*/m-logcat-console/01_logcat_process/complete.log`가 있어야 한다. 현재 충족: `agent-task/archive/2026/06/m-logcat-console/01_logcat_process/complete.log`.

## LOGCAT_STREAM-1 Shell Device Selection

### 문제

[devices_page.dart](/config/workspace/appsok/lib/src/features/devices/devices_page.dart:368)의 logcat 버튼은 no-op이고, [app_shell.dart](/config/workspace/appsok/lib/src/features/app_shell.dart:201)는 console을 `const ConsolePage()`로 고정한다.

```dart
368  _CompactIconButton(
369    tooltip: 'logcat',
370    icon: Icons.terminal,
371    onPressed: () {},
372  ),
```

```dart
201  const _ShellPage(
202    label: '콘솔',
203    icon: Icons.terminal_outlined,
204    selectedIcon: Icons.terminal,
205    child: ConsolePage(),
206  ),
```

### 해결 방법

`DevicesPage`에 `ValueChanged<AdbDevice>? onLogcatRequested`를 추가하고 ready device의 logcat 버튼만 활성화한다. `AppSokShell`은 `_selectedConsoleDevice`를 저장하고 `_selectedIndex = 2`로 이동하며, `ConsolePage`에 device와 starter를 전달한다.

```dart
void _handleLogcatRequested(AdbDevice device) {
  setState(() {
    _selectedConsoleDevice = device;
    _selectedIndex = 2;
  });
}
```

### 수정 파일 및 체크리스트

- [ ] `lib/src/features/devices/devices_page.dart`: `onLogcatRequested` prop과 ready device button enablement 추가.
- [ ] `lib/src/features/app_shell.dart`: selected console device state와 navigation handoff 추가.
- [ ] `lib/src/app.dart`: `AdbService.startLogcat` starter를 shell에 전달.
- [ ] `test/widget_test.dart`: ready device logcat 버튼이 console로 이동하고 device label을 표시하는 test 추가.

### 테스트 작성

- 작성: `test/widget_test.dart`
- 테스트 이름 후보: `ready device logcat button opens console for that device`
- assertion: ready device button tap 후 navigation marker는 `콘솔`, selected device serial/display name이 console에 표시된다.

### 중간 검증

```bash
flutter test test/widget_test.dart
```

예상: shell/device widget tests 통과.

## LOGCAT_STREAM-2 Live Stream Lifecycle

### 문제

[console_page.dart](/config/workspace/appsok/lib/src/features/console/console_page.dart:69)는 `_logLines` 정적 목록만 사용하고, stream subscription이나 dispose cleanup이 없다.

```dart
69  child: ListView.builder(
70    padding: const EdgeInsets.all(16),
71    itemCount: _logLines.length,
72    itemBuilder: (context, index) {
73      final line = _logLines[index];
```

### 해결 방법

`ConsolePage`를 `StatefulWidget`으로 바꾸고 `AdbLogcatSessionStarter? logcatStarter`, `AdbDevice? device`를 받는다. `initState`/`didUpdateWidget`에서 device가 바뀌면 기존 subscription/session을 정리하고 새 stream을 시작한다. append buffer는 MVP에서 1000줄 정도로 cap을 두어 무한 메모리 증가를 막는다.

```dart
class ConsolePage extends StatefulWidget {
  const ConsolePage({super.key, this.device, this.logcatStarter});

  final AdbDevice? device;
  final AdbLogcatSessionStarter? logcatStarter;
}
```

### 수정 파일 및 체크리스트

- [ ] `lib/src/features/console/console_page.dart`: Stateful conversion, stream start/append, error state, stop/dispose 처리.
- [ ] `test/console_page_test.dart`: fake session stream append와 dispose stop 검증 추가.
- [ ] `test/widget_test.dart`: navigation away 또는 device 변경 시 stop counter 증가 검증 추가.

### 테스트 작성

- 작성: `test/console_page_test.dart`, `test/widget_test.dart`
- assertion: stream line이 화면에 append되고, widget dispose/device change 후 fake session `stopCalls == 1`.

### 중간 검증

```bash
flutter test test/console_page_test.dart test/widget_test.dart
```

예상: console focused test와 shell widget tests 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `lib/src/app.dart` | LOGCAT_STREAM-1 |
| `lib/src/features/app_shell.dart` | LOGCAT_STREAM-1 |
| `lib/src/features/devices/devices_page.dart` | LOGCAT_STREAM-1 |
| `lib/src/features/console/console_page.dart` | LOGCAT_STREAM-2 |
| `test/widget_test.dart` | LOGCAT_STREAM-1, LOGCAT_STREAM-2 |
| `test/console_page_test.dart` | LOGCAT_STREAM-2 |

## 최종 검증

```bash
flutter analyze
flutter test test/widget_test.dart test/console_page_test.dart
```

예상: analyzer issue 없음, targeted widget tests 통과.

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