feat(client): iop_console 패키지로 클라이언트 UI를 분리한다
- IopConsoleShell, IopAgentPanel 컴포넌트로 main.dart 리팩토링 - packages/iop_console 새 패키지 추가 - 클라이언트 pubspec에 iop_console 의존성 추가 - 로드맵 및 마일스톤에 embeddable-iop-console-workbench 추가 - 관련 규칙 및 문서 업데이트
This commit is contained in:
parent
0ecd9d481c
commit
49872ae120
16 changed files with 614 additions and 139 deletions
|
|
@ -226,12 +226,14 @@ NomadCode
|
|||
- `mock` adapter와 dummy/TODO 구현은 개발 단계에서 정상적인 구성이다.
|
||||
- Ollama/vLLM 등 모델 runtime adapter는 단계적으로 확장한다.
|
||||
- Control Plane은 향후 여러 Edge 관리와 Client 제공을 위해 추가된다.
|
||||
- `packages/iop_console`에는 공통 `agent_shell` 패키지를 사용하는 `IopConsoleShell`과 `IopAgentPanel` scaffold가 있다. 이 패키지는 IOP 운영/유지보수 agent 표면의 시작점이며, IOP 단독 앱과 NomadCode 같은 외부 소비자에 임베드되는 UI 모두에서 재사용 가능한 방향으로 둔다.
|
||||
|
||||
현재 앱 구성은 다음과 같다.
|
||||
|
||||
| 경로 | 현재 의미 |
|
||||
|---|---|
|
||||
| `apps/client` | IOP Client UI의 기준 구현인 Flutter 애플리케이션. Flutter Web 산출물이 compose `web` 서비스로 배포된다 |
|
||||
| `apps/client` | IOP Client UI의 기준 구현인 Flutter 애플리케이션. `packages/iop_console`을 mount하며 Flutter Web 산출물이 compose `web` 서비스로 배포된다 |
|
||||
| `packages/iop_console` | IOP-owned embeddable Flutter console package. 좌측 rail shell과 `agent_shell` 기반 IOP agent panel을 제공한다 |
|
||||
| `apps/node` | Edge에 연결되어 adapter execution을 수행하는 Node agent |
|
||||
| `apps/edge` | Node/domain agent registry, 설정 전달, bootstrap, routing, stream relay를 담당하는 Edge skeleton |
|
||||
| `apps/control-plane` | 여러 Edge를 연결하고 Client과 통신할 Go 기반 운영 제어 서버 스캐폴드 |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
domain: client
|
||||
last_rule_review_commit: aaf35e61a91d33b81b64b1054649f7e28ad421c0
|
||||
last_rule_review_commit: 0ecd9d481c75d290fae760f9cbf697f92ae368aa
|
||||
last_rule_updated_at: 2026-05-31
|
||||
---
|
||||
|
||||
|
|
@ -13,6 +13,8 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
## 포함 경로
|
||||
|
||||
- `apps/client/lib/` — Flutter app source, build-time client config, proto-socket wire client, generated Dart proto 사용, client integration code
|
||||
- `apps/client/lib/src/features/` — IOP client feature UI와 embeddable panel widget
|
||||
- `packages/iop_console/` — IOP-owned embeddable Flutter console package, left-rail shell, agent panel widget
|
||||
- `apps/client/test/` — Flutter widget/config/wire/integration 테스트
|
||||
- `apps/client/web/` — Flutter Web shell과 web asset
|
||||
- `apps/client/android/` — Android runner와 Firebase 설정 표면
|
||||
|
|
@ -46,6 +48,8 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
- `clientParserMap` — Client-Control Plane proto message parser map
|
||||
- `apps/client/lib/gen/proto/iop/*.dart` — `make proto-dart`로 생성되는 Dart protobuf binding
|
||||
- `MattermostPushHostIntegration` / `MattermostPushPluginClient` — Nexo messaging / Mattermost-compatible push integration host
|
||||
- `IopConsoleShell` — IOP 단독 앱과 외부 임베더가 공유할 수 있는 좌측 rail console shell
|
||||
- `IopAgentPanel` — 공통 `agent_shell` package를 사용한 IOP 운영 agent panel scaffold
|
||||
- `apps/client/Dockerfile` — sibling `proto-socket/dart` path dependency를 포함해 Flutter Web artifact를 빌드하는 이미지
|
||||
|
||||
## 유지할 패턴
|
||||
|
|
@ -56,12 +60,18 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
- Dart protobuf binding은 `proto/iop/*.proto`에서 생성한다. proto 계약 변경 시 `make proto-dart` 산출물과 Go 생성물 갱신 여부를 함께 확인한다.
|
||||
- `apps/client/lib/gen/proto/iop/*.dart` 생성물은 사람이 직접 수정하지 않는다.
|
||||
- Mattermost/Nexo 연동은 `lib/src/integrations/` 아래 통합 모듈로 둔다. IOP client app shell을 NomadCode 전용 UX로 바꾸지 않는다.
|
||||
- `packages/iop_console`은 IOP UI의 공개 Flutter widget/package 경계다. IOP 단독 앱은 이 package를 mount하고, 외부 소비자는 이 package 또는 동등한 IOP-owned widget boundary를 통해 조립한다.
|
||||
- `agent_shell`은 제품 중립 chat/agent interaction shell로만 사용한다. IOP client에는 IOP 운영/유지보수 capability와 panel widget을 담고, NomadCode의 workbench/right-rail layout은 가져오지 않는다.
|
||||
- IOP UI를 NomadCode에 제공해야 할 때는 IOP-owned widget/package 경계로 노출하고, NomadCode product shell 내부 구현을 IOP client에 복제하지 않는다.
|
||||
- client 변경 후에는 변경 범위에 맞게 `flutter test` 또는 `make client-test`를 확인한다. Web build, Dockerfile, compose 경로를 바꾸면 `make client-build-web` 또는 해당 build 경로를 확인한다.
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
|
||||
- **control-plane**: Control Plane은 `/client` WS endpoint, HTTP 상태 endpoint, Edge connection registry를 제공한다. Client는 이를 소비하는 UI/UX와 client-side wire wrapper를 소유한다.
|
||||
- **platform-common**: protobuf 원본 계약은 platform-common이 소유한다. Client는 해당 계약에서 생성된 Dart binding을 사용한다.
|
||||
- **agent-shell**: `agent_shell` sibling package는 공통 chat/agent shell widget과 message model만 제공한다. IOP-specific operation semantics와 Control Plane 연동은 client domain에 남긴다.
|
||||
- **platform-common**: `packages/iop_console`은 Flutter UI package이므로 Go 공통 설정/proto/helper 패키지와 섞지 않는다.
|
||||
- **NomadCode**: NomadCode는 IOP의 중요한 UI 소비자일 수 있지만, IOP client는 NomadCode 전용 navigation, workspace, web context UX를 소유하지 않는다.
|
||||
- **testing**: client test/build target, `scripts/dev/web.sh`, compose 기반 local dev 검증 기준은 testing domain rule을 따른다.
|
||||
- **edge/node**: Client는 Edge와 Node를 직접 제어하지 않고 Control Plane 표면을 통해 관찰/명령한다.
|
||||
|
||||
|
|
@ -69,6 +79,7 @@ IOP의 공식 Flutter client UI/UX 영역이다. Control Plane HTTP/WS endpoint
|
|||
|
||||
- Client에서 Edge 또는 Node TCP/protobuf 내부 transport에 직접 연결하지 않는다.
|
||||
- Client를 NomadCode 전용 shell, project workspace, diff/PR/branch/commit UX 중심으로 만들지 않는다.
|
||||
- NomadCode right rail, workspace tab, web context 조작 UX를 IOP client domain에 직접 구현하지 않는다.
|
||||
- `apps/client/lib/gen/proto/iop/*.dart` 생성 파일을 직접 수정하지 않는다.
|
||||
- Mattermost smoke credential 같은 local/private 값을 tracked asset이나 README에 기록하지 않는다.
|
||||
- Control Plane server 상태 저장, Edge registry, run dispatch, stream relay 구현을 client domain으로 끌어오지 않는다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
domain: platform-common
|
||||
last_rule_review_commit: aaf35e61a91d33b81b64b1054649f7e28ad421c0
|
||||
last_rule_review_commit: 0ecd9d481c75d290fae760f9cbf697f92ae368aa
|
||||
last_rule_updated_at: 2026-05-31
|
||||
---
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ last_rule_updated_at: 2026-05-31
|
|||
- `apps/edge/` — 실행 그룹 컨트롤러와 node registry
|
||||
- `apps/control-plane/` — 중앙 제어면 앱 구현 영역
|
||||
- `apps/client/` — Flutter client app과 Dart protobuf 생성물 사용 영역
|
||||
- `packages/iop_console/` — Flutter client/console UI package이므로 client domain 소유
|
||||
- `apps/worker/` — worker 앱 구현 예정 영역
|
||||
|
||||
## 주요 구성 요소
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@
|
|||
| `apps/edge/**` | edge | `agent-ops/rules/project/domain/edge/rules.md` |
|
||||
| `apps/control-plane/**` | control-plane | `agent-ops/rules/project/domain/control-plane/rules.md` |
|
||||
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||
| `packages/iop_console/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||
| `packages/**` | platform-common | `agent-ops/rules/project/domain/platform-common/rules.md` |
|
||||
| `proto/**` | platform-common | `agent-ops/rules/project/domain/platform-common/rules.md` |
|
||||
| `configs/**` | platform-common | `agent-ops/rules/project/domain/platform-common/rules.md` |
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
- [계획] Control Plane과 Client
|
||||
- Phase: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-client.md`
|
||||
- [계획] Embeddable IOP Console Workbench
|
||||
- Phase: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/embeddable-iop-console-workbench.md`
|
||||
|
||||
## 선택 규칙
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@
|
|||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-client.md`
|
||||
- 요약: 여러 Edge를 연결하고 관찰하며 Edge 설정 변경, 명령 전달, 이벤트 수신을 담당하는 중앙 제어면과 Flutter client 운영면을 구축한다.
|
||||
|
||||
- [계획] Embeddable IOP Console Workbench
|
||||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/embeddable-iop-console-workbench.md`
|
||||
- 요약: IOP standalone 앱은 application shell로 유지하고, 실제 IOP 관리 기능과 UI 위젯은 외부 host가 선택적으로 삽입할 수 있는 Flutter package로 분리한다.
|
||||
|
||||
- [계획] 정책, 이력, 감사
|
||||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/policy-history-audit.md`
|
||||
- 요약: 권한, 정책, 실행 이력, 감사 로그를 제품 운영에 필요한 수준으로 확장한다.
|
||||
|
|
@ -53,5 +57,6 @@
|
|||
|
||||
- Control Plane은 Node를 직접 스케줄링하지 않고 Edge를 통해 관찰/제어한다.
|
||||
- Flutter client는 IOP 자체 운영 UI/UX이며 NomadCode 전용 shell로 만들지 않는다.
|
||||
- IOP 관리 UI는 standalone application shell과 embeddable package 경계를 분리해 NomadCode 같은 host가 선택적으로 포함할 수 있게 한다.
|
||||
- Multi-Edge 운영은 Edge의 로컬 상태 소유권을 침범하지 않는 범위에서 확장한다.
|
||||
- NomadCode Flutter client 복제/표준화 작업은 별도 프로젝트 범위로 분리하고, IOP Phase에서는 필요한 경우 확정 산출물만 후속 Milestone으로 반영한다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
# Milestone: Embeddable IOP Console Workbench
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: `agent-roadmap/ROADMAP.md`
|
||||
- Phase: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
|
||||
## 목표
|
||||
|
||||
IOP standalone 앱은 application shell로 유지하되, 실제 IOP 관리 기능과 UI 위젯 전체는 Flutter package 형태로 분리한다. 이 package는 IOP 앱에서는 좌측 icon rail + center content 구조로 보이고, NomadCode 같은 host 앱에서는 같은 native Flutter 위젯이 별도 제품 shell 안에 그대로 들어갈 수 있어야 한다.
|
||||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- `iop/apps/client`를 standalone application shell로 유지하는 구조
|
||||
- IOP 관리 화면, route/controller, rail item, agent capability를 embeddable Flutter package로 분리하는 구조
|
||||
- IOP standalone에서는 좌측 icon rail만 두고 중앙 content를 전환하는 workbench layout
|
||||
- host 앱이 IOP package를 선택적으로 import해 native Flutter widget으로 삽입할 수 있는 public API
|
||||
- 공통 agent shell package와 IOP agent capability pack의 연결 경계
|
||||
- IOP package가 NomadCode 제품 shell, WebView 조작 UX, workflow/task UX를 알지 못하게 하는 의존성 경계
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [package-boundary] IOP console package boundary
|
||||
|
||||
IOP 운영 경험을 application shell에서 분리해, standalone 앱과 외부 host가 같은 관리 UI를 재사용할 수 있는 package 경계를 만든다.
|
||||
|
||||
- [ ] [console-package] IOP 관리 UI를 `MaterialApp`/global bootstrap 없이 import 가능한 Flutter package로 분리한다. 검증: package public export가 app shell 없이 widget/controller/rail item 단위로 소비 가능하다.
|
||||
- [ ] [standalone-shell] `iop/apps/client`는 Firebase/SystemChrome/app bootstrap과 standalone theme/navigation만 담당하고, 실제 화면은 IOP console package를 감싸서 실행한다. 검증: standalone 앱에서 IOP console이 좌측 rail + center content 구조로 표시된다.
|
||||
- [ ] [host-agnostic] IOP console package는 NomadCode, Project/Workspace, WebView 조작, product workflow 개념을 import하거나 전제하지 않는다. 검증: package dependency graph에 NomadCode 전용 package가 없다.
|
||||
|
||||
### Epic: [workbench-layout] IOP left-rail workbench
|
||||
|
||||
IOP standalone 화면은 좌측 icon rail을 기준으로 중앙 content를 전환하는 운영 workbench로 정리한다.
|
||||
|
||||
- [ ] [left-rail] IOP standalone layout은 좌측 icon rail과 center content host를 기본 구조로 둔다.
|
||||
- [ ] [content-switch] Overview, Edge, Node, Runtime, Execution, Logs, Agent, Settings 같은 IOP 운영 영역은 rail item 선택에 따라 중앙 content를 전환한다.
|
||||
- [ ] [stable-widgets] rail item, tooltip, selected state, content host는 host layout 크기에 따라 안정적으로 배치된다. 검증: desktop/mobile 또는 narrow/wide viewport에서 rail과 content가 겹치지 않는다.
|
||||
|
||||
### Epic: [agent-capability] IOP agent capability pack
|
||||
|
||||
공통 agent shell은 외부 package로 두고, IOP는 자신의 운영 capability만 제공한다.
|
||||
|
||||
- [ ] [agent-shell-link] IOP console package는 공통 `agent_shell` 계열 package를 통해 채팅형 agent surface를 열 수 있다.
|
||||
- [ ] [iop-capabilities] IOP agent capability는 Edge/Node 상태, runtime/adapter/target, execution, logs, maintenance command 같은 IOP 책임 범위만 포함한다.
|
||||
- [ ] [capability-api] IOP capability pack은 host가 agent shell registry에 꽂을 수 있는 public API를 제공한다. 검증: IOP standalone과 외부 host가 같은 IOP capability pack을 등록할 수 있다.
|
||||
|
||||
### Epic: [embedding-contract] External host embedding contract
|
||||
|
||||
NomadCode 같은 외부 host가 IOP UI를 optional feature로 넣거나 빼도 빌드 경계가 깨지지 않도록 한다.
|
||||
|
||||
- [ ] [public-widget] IOP package는 `IopConsole`, `IopConsoleController`, `IopRailItem` 또는 동등한 public entry를 제공한다.
|
||||
- [ ] [config-injection] host는 Control Plane endpoint, auth/token reference, theme adapter, navigation callback을 주입하고 IOP package는 host 전역 상태를 직접 소유하지 않는다.
|
||||
- [ ] [optional-consumer] IOP package는 optional consumer인 NomadCode 없이도 단독으로 테스트되고 빌드된다. 검증: IOP repository 안에서 package test/analyze 경로가 독립적으로 통과한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다.
|
||||
- 리뷰 필요:
|
||||
- [ ] 사용자가 완료 결과를 확인했다
|
||||
- [ ] archive 이동을 승인했다
|
||||
- 리뷰 코멘트: 없음
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- NomadCode의 우측 rail, titlebar, workflow/project/webview UX 구현
|
||||
- NomadCode default build와 IOP 포함 build의 조립 전략 구현
|
||||
- IOP 내부 모델 라우팅, RAG, MCP/tool policy, output validation 세부 구현
|
||||
- 런타임 동적 plugin framework 도입
|
||||
- IOP package가 NomadCode 전용 화면을 직접 import하거나 소유하는 구조
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `apps/client/`, `packages/`, `proto/iop/`, `apps/control-plane/`
|
||||
- 표준선(선택): IOP는 범용 추론/자동화 운영 엔진이며 NomadCode 전용 Agent Shell이 아니다.
|
||||
- 표준선(선택): shared UI는 application shell이 아니라 package/widget/controller/capability 단위로 재사용한다.
|
||||
- 표준선(선택): IOP standalone은 좌측 icon rail + center content 구조를 기본 workbench로 둔다.
|
||||
- 선행 작업: Control Plane과 Client, Flutter-first Client 마이그레이션, Control Plane-Edge Wire Baseline
|
||||
- 후속 작업: NomadCode Workbench Shell and Optional IOP Composition, Multi-Edge 운영
|
||||
- 확인 필요: 후속 구현 계획에서 실제 package 이름과 최초 rail item 목록을 확정한다.
|
||||
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
IOP(Inference Operations Platform)의 공식 Client UI 애플리케이션입니다. Flutter를 사용하여 다중 플랫폼(Web, Desktop, Mobile)을 단일 코드베이스로 지원합니다.
|
||||
|
||||
## Agent Shell 구성
|
||||
|
||||
이 client는 IOP-owned Flutter package인 `packages/iop_console`을 path dependency로 사용합니다. `iop_console`은 workspace sibling package인 `agent_shell`을 사용해 `IopConsoleShell`과 `IopAgentPanel`을 제공하며, IOP 운영/유지보수 agent 화면을 단독 IOP 앱과 외부 임베딩 소비자에서 같은 위젯 경계로 조립하기 위한 시작점입니다.
|
||||
|
||||
IOP client는 NomadCode 전용 workbench layout을 소유하지 않습니다. IOP 쪽 책임은 Control Plane을 통해 Edge/Node 운영 상태와 유지보수 작업을 다루는 agent capability와 panel widget을 제공하는 것이고, NomadCode 같은 소비자는 `iop_console` 또는 향후 IOP console package를 자신의 shell 안에 선택적으로 mount합니다.
|
||||
|
||||
## 개발 및 검증 명령
|
||||
|
||||
### 의존성 설치
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:iop_console/iop_console.dart';
|
||||
|
||||
import 'client_config.dart';
|
||||
import 'iop_wire/client_wire_client.dart';
|
||||
|
|
@ -191,150 +192,152 @@ class _ClientHomePageState extends State<ClientHomePage>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0F172A), Color(0xFF1E1B4B)],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 32.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Top Header Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'IOP Client',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Inference Operations Platform',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.grey[400],
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Health Status Indicator
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(
|
||||
0xFF10B981,
|
||||
).withOpacity(0.15 * _pulseAnimation.value),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: const Color(
|
||||
0xFF10B981,
|
||||
).withOpacity(0.5 * _pulseAnimation.value),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF10B981),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'HEALTH: OK',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF10B981),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
return IopConsoleShell(
|
||||
overview: _buildOverviewContent(),
|
||||
agent: const IopAgentPanel(),
|
||||
);
|
||||
}
|
||||
|
||||
// Dashboard Cards Section
|
||||
Expanded(
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
Widget _buildOverviewContent() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF0F172A), Color(0xFF1E1B4B)],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Top Header Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildEndpointCard(
|
||||
title: 'Control Plane Connection',
|
||||
icon: Icons.cloud_queue_rounded,
|
||||
accentColor: const Color(0xFF6366F1),
|
||||
children: [
|
||||
_buildConfigRow(
|
||||
'Control Plane HTTP Endpoint',
|
||||
ClientConfig.controlPlaneHttpUrl,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildConfigRow(
|
||||
'Control Plane Wire WebSocket Endpoint',
|
||||
ClientConfig.controlPlaneWireUrl,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Colors.white10),
|
||||
const SizedBox(height: 12),
|
||||
_buildWireStatusSection(),
|
||||
],
|
||||
const Text(
|
||||
'IOP Client',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildEndpointCard(
|
||||
title: 'System Information',
|
||||
icon: Icons.info_outline_rounded,
|
||||
accentColor: const Color(0xFF3B82F6),
|
||||
children: [
|
||||
_buildConfigRow('Framework', 'Flutter Web & Native'),
|
||||
const SizedBox(height: 12),
|
||||
_buildConfigRow('UI Status', 'Scaffold Active'),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Inference Operations Platform',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: Colors.grey[400],
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Footer
|
||||
Center(
|
||||
child: Text(
|
||||
'© 2026 Antigravity & Toki Labs. All rights reserved.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
// Health Status Indicator
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(
|
||||
0xFF10B981,
|
||||
).withOpacity(0.15 * _pulseAnimation.value),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: const Color(
|
||||
0xFF10B981,
|
||||
).withOpacity(0.5 * _pulseAnimation.value),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF10B981),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'HEALTH: OK',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF10B981),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Dashboard Cards Section
|
||||
Expanded(
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
children: [
|
||||
_buildEndpointCard(
|
||||
title: 'Control Plane Connection',
|
||||
icon: Icons.cloud_queue_rounded,
|
||||
accentColor: const Color(0xFF6366F1),
|
||||
children: [
|
||||
_buildConfigRow(
|
||||
'Control Plane HTTP Endpoint',
|
||||
ClientConfig.controlPlaneHttpUrl,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildConfigRow(
|
||||
'Control Plane Wire WebSocket Endpoint',
|
||||
ClientConfig.controlPlaneWireUrl,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(color: Colors.white10),
|
||||
const SizedBox(height: 12),
|
||||
_buildWireStatusSection(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildEndpointCard(
|
||||
title: 'System Information',
|
||||
icon: Icons.info_outline_rounded,
|
||||
accentColor: const Color(0xFF3B82F6),
|
||||
children: [
|
||||
_buildConfigRow('Framework', 'Flutter Web & Native'),
|
||||
const SizedBox(height: 12),
|
||||
_buildConfigRow('UI Status', 'Scaffold Active'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Footer
|
||||
Center(
|
||||
child: Text(
|
||||
'© 2026 Antigravity & Toki Labs. All rights reserved.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.59"
|
||||
agent_shell:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../../agent-shell"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -160,6 +167,13 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
iop_console:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../../packages/iop_console"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ dependencies:
|
|||
nexo_messaging:
|
||||
path: ../../../nexo/packages/messaging_flutter
|
||||
|
||||
# Shared embeddable agent interaction shell.
|
||||
agent_shell:
|
||||
path: ../../../agent-shell
|
||||
|
||||
# IOP-owned embeddable console widgets.
|
||||
iop_console:
|
||||
path: ../../packages/iop_console
|
||||
|
||||
protobuf: ^6.0.0
|
||||
fixnum: ^1.1.1
|
||||
proto_socket:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:iop_client/main.dart';
|
||||
import 'package:iop_client/client_config.dart';
|
||||
|
|
@ -108,4 +109,20 @@ void main() {
|
|||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Client App opens IOP agent panel from the left rail', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
|
||||
await tester.pumpWidget(IopClientApp(testClient: fakeClient));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byIcon(Icons.smart_toy_outlined));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Ask about IOP operations'), findsOneWidget);
|
||||
expect(find.textContaining('IOP agent surface is ready'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
2
packages/iop_console/lib/iop_console.dart
Normal file
2
packages/iop_console/lib/iop_console.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export 'src/iop_agent_panel.dart';
|
||||
export 'src/iop_console_shell.dart';
|
||||
27
packages/iop_console/lib/src/iop_agent_panel.dart
Normal file
27
packages/iop_console/lib/src/iop_agent_panel.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:agent_shell/agent_shell.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class IopAgentPanel extends StatelessWidget {
|
||||
final ValueChanged<String>? onSubmit;
|
||||
|
||||
const IopAgentPanel({super.key, this.onSubmit});
|
||||
|
||||
static const _messages = <AgentMessage>[
|
||||
AgentMessage(
|
||||
id: 'iop-agent-intro',
|
||||
role: AgentMessageRole.assistant,
|
||||
text:
|
||||
'IOP agent surface is ready. Edge, Node, Runtime, and Execution capabilities can be registered here.',
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AgentShell(
|
||||
messages: _messages,
|
||||
busy: false,
|
||||
placeholder: 'Ask about IOP operations',
|
||||
onSubmit: onSubmit,
|
||||
);
|
||||
}
|
||||
}
|
||||
263
packages/iop_console/lib/src/iop_console_shell.dart
Normal file
263
packages/iop_console/lib/src/iop_console_shell.dart
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'iop_agent_panel.dart';
|
||||
|
||||
enum IopConsoleSection {
|
||||
overview,
|
||||
edges,
|
||||
nodes,
|
||||
runtime,
|
||||
maintenance,
|
||||
agent,
|
||||
settings,
|
||||
}
|
||||
|
||||
class IopConsoleShell extends StatefulWidget {
|
||||
final Widget overview;
|
||||
final Widget? edges;
|
||||
final Widget? nodes;
|
||||
final Widget? runtime;
|
||||
final Widget? maintenance;
|
||||
final Widget? agent;
|
||||
final Widget? settings;
|
||||
final IopConsoleSection initialSection;
|
||||
|
||||
const IopConsoleShell({
|
||||
super.key,
|
||||
required this.overview,
|
||||
this.edges,
|
||||
this.nodes,
|
||||
this.runtime,
|
||||
this.maintenance,
|
||||
this.agent,
|
||||
this.settings,
|
||||
this.initialSection = IopConsoleSection.overview,
|
||||
});
|
||||
|
||||
@override
|
||||
State<IopConsoleShell> createState() => _IopConsoleShellState();
|
||||
}
|
||||
|
||||
class _IopConsoleShellState extends State<IopConsoleShell> {
|
||||
late IopConsoleSection _section = widget.initialSection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0F172A),
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_IopConsoleRail(selected: _section, onSelect: _select),
|
||||
Expanded(child: _buildContent()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _select(IopConsoleSection section) {
|
||||
setState(() {
|
||||
_section = section;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
return switch (_section) {
|
||||
IopConsoleSection.overview => widget.overview,
|
||||
IopConsoleSection.edges =>
|
||||
widget.edges ??
|
||||
const _IopPlaceholder(
|
||||
icon: Icons.device_hub_outlined,
|
||||
title: 'Edges',
|
||||
subtitle: 'Edge group status and command surfaces mount here.',
|
||||
),
|
||||
IopConsoleSection.nodes =>
|
||||
widget.nodes ??
|
||||
const _IopPlaceholder(
|
||||
icon: Icons.memory_outlined,
|
||||
title: 'Nodes',
|
||||
subtitle:
|
||||
'Node health, runtime inventory, and execution lanes mount here.',
|
||||
),
|
||||
IopConsoleSection.runtime =>
|
||||
widget.runtime ??
|
||||
const _IopPlaceholder(
|
||||
icon: Icons.speed_outlined,
|
||||
title: 'Runtime',
|
||||
subtitle:
|
||||
'Adapter, target, profile, and routing controls mount here.',
|
||||
),
|
||||
IopConsoleSection.maintenance =>
|
||||
widget.maintenance ??
|
||||
const _IopPlaceholder(
|
||||
icon: Icons.build_circle_outlined,
|
||||
title: 'Maintenance',
|
||||
subtitle:
|
||||
'Field diagnostics, bootstrap, and repair commands mount here.',
|
||||
),
|
||||
IopConsoleSection.agent => widget.agent ?? const IopAgentPanel(),
|
||||
IopConsoleSection.settings =>
|
||||
widget.settings ??
|
||||
const _IopPlaceholder(
|
||||
icon: Icons.tune,
|
||||
title: 'Settings',
|
||||
subtitle:
|
||||
'Control Plane endpoint and operator preferences mount here.',
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _IopConsoleRail extends StatelessWidget {
|
||||
final IopConsoleSection selected;
|
||||
final ValueChanged<IopConsoleSection> onSelect;
|
||||
|
||||
const _IopConsoleRail({required this.selected, required this.onSelect});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 60,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF111827),
|
||||
border: Border(right: BorderSide(color: Color(0xFF263043))),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
_RailButton(
|
||||
tooltip: 'Overview',
|
||||
icon: Icons.dashboard_outlined,
|
||||
selected: selected == IopConsoleSection.overview,
|
||||
onPressed: () => onSelect(IopConsoleSection.overview),
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Edges',
|
||||
icon: Icons.device_hub_outlined,
|
||||
selected: selected == IopConsoleSection.edges,
|
||||
onPressed: () => onSelect(IopConsoleSection.edges),
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Nodes',
|
||||
icon: Icons.memory_outlined,
|
||||
selected: selected == IopConsoleSection.nodes,
|
||||
onPressed: () => onSelect(IopConsoleSection.nodes),
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Runtime',
|
||||
icon: Icons.speed_outlined,
|
||||
selected: selected == IopConsoleSection.runtime,
|
||||
onPressed: () => onSelect(IopConsoleSection.runtime),
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Maintenance',
|
||||
icon: Icons.build_circle_outlined,
|
||||
selected: selected == IopConsoleSection.maintenance,
|
||||
onPressed: () => onSelect(IopConsoleSection.maintenance),
|
||||
),
|
||||
const Spacer(),
|
||||
_RailButton(
|
||||
tooltip: 'Agent',
|
||||
icon: Icons.smart_toy_outlined,
|
||||
selected: selected == IopConsoleSection.agent,
|
||||
onPressed: () => onSelect(IopConsoleSection.agent),
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Settings',
|
||||
icon: Icons.tune,
|
||||
selected: selected == IopConsoleSection.settings,
|
||||
onPressed: () => onSelect(IopConsoleSection.settings),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RailButton extends StatelessWidget {
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final bool selected;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _RailButton({
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.selected,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = selected ? const Color(0xFF10B981) : Colors.white60;
|
||||
return Tooltip(
|
||||
message: 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
|
||||
? const Color(0xFF10B981).withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IopPlaceholder extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
const _IopPlaceholder({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFF0F172A),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: const Color(0xFF10B981), size: 40),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white60, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
packages/iop_console/pubspec.yaml
Normal file
22
packages/iop_console/pubspec.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: iop_console
|
||||
description: "Embeddable Flutter console widgets for IOP operations."
|
||||
version: 0.1.0
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.3
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
agent_shell:
|
||||
path: ../../../agent-shell
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
Loading…
Reference in a new issue