diff --git a/README.md b/README.md index 65a39d0..acec463 100644 --- a/README.md +++ b/README.md @@ -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 기반 운영 제어 서버 스캐폴드 | diff --git a/agent-ops/rules/project/domain/client/rules.md b/agent-ops/rules/project/domain/client/rules.md index 28927d4..e02aa18 100644 --- a/agent-ops/rules/project/domain/client/rules.md +++ b/agent-ops/rules/project/domain/client/rules.md @@ -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으로 끌어오지 않는다. diff --git a/agent-ops/rules/project/domain/platform-common/rules.md b/agent-ops/rules/project/domain/platform-common/rules.md index 3a55eca..b431a26 100644 --- a/agent-ops/rules/project/domain/platform-common/rules.md +++ b/agent-ops/rules/project/domain/platform-common/rules.md @@ -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 앱 구현 예정 영역 ## 주요 구성 요소 diff --git a/agent-ops/rules/project/rules.md b/agent-ops/rules/project/rules.md index 9756721..78122f8 100644 --- a/agent-ops/rules/project/rules.md +++ b/agent-ops/rules/project/rules.md @@ -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` | diff --git a/agent-roadmap/current.md b/agent-roadmap/current.md index ef06abb..8f348d5 100644 --- a/agent-roadmap/current.md +++ b/agent-roadmap/current.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` ## 선택 규칙 diff --git a/agent-roadmap/phase/control-plane-portal-ops/PHASE.md b/agent-roadmap/phase/control-plane-portal-ops/PHASE.md index 0469462..17585e8 100644 --- a/agent-roadmap/phase/control-plane-portal-ops/PHASE.md +++ b/agent-roadmap/phase/control-plane-portal-ops/PHASE.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으로 반영한다. diff --git a/agent-roadmap/phase/control-plane-portal-ops/milestones/embeddable-iop-console-workbench.md b/agent-roadmap/phase/control-plane-portal-ops/milestones/embeddable-iop-console-workbench.md new file mode 100644 index 0000000..2b2a681 --- /dev/null +++ b/agent-roadmap/phase/control-plane-portal-ops/milestones/embeddable-iop-console-workbench.md @@ -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 목록을 확정한다. diff --git a/apps/client/README.md b/apps/client/README.md index 2806227..359bc4f 100644 --- a/apps/client/README.md +++ b/apps/client/README.md @@ -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합니다. + ## 개발 및 검증 명령 ### 의존성 설치 diff --git a/apps/client/lib/main.dart b/apps/client/lib/main.dart index 84048d5..d753003 100644 --- a/apps/client/lib/main.dart +++ b/apps/client/lib/main.dart @@ -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 @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]), + ), + ), + ], ), ), ), diff --git a/apps/client/pubspec.lock b/apps/client/pubspec.lock index c87d652..2572038 100644 --- a/apps/client/pubspec.lock +++ b/apps/client/pubspec.lock @@ -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: diff --git a/apps/client/pubspec.yaml b/apps/client/pubspec.yaml index 9c9b624..28f4b70 100644 --- a/apps/client/pubspec.yaml +++ b/apps/client/pubspec.yaml @@ -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: diff --git a/apps/client/test/widget_test.dart b/apps/client/test/widget_test.dart index aa02c58..6de9a1f 100644 --- a/apps/client/test/widget_test.dart +++ b/apps/client/test/widget_test.dart @@ -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); + }); } diff --git a/packages/iop_console/lib/iop_console.dart b/packages/iop_console/lib/iop_console.dart new file mode 100644 index 0000000..561849e --- /dev/null +++ b/packages/iop_console/lib/iop_console.dart @@ -0,0 +1,2 @@ +export 'src/iop_agent_panel.dart'; +export 'src/iop_console_shell.dart'; diff --git a/packages/iop_console/lib/src/iop_agent_panel.dart b/packages/iop_console/lib/src/iop_agent_panel.dart new file mode 100644 index 0000000..6162834 --- /dev/null +++ b/packages/iop_console/lib/src/iop_agent_panel.dart @@ -0,0 +1,27 @@ +import 'package:agent_shell/agent_shell.dart'; +import 'package:flutter/material.dart'; + +class IopAgentPanel extends StatelessWidget { + final ValueChanged? onSubmit; + + const IopAgentPanel({super.key, this.onSubmit}); + + static const _messages = [ + 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, + ); + } +} diff --git a/packages/iop_console/lib/src/iop_console_shell.dart b/packages/iop_console/lib/src/iop_console_shell.dart new file mode 100644 index 0000000..faac8b8 --- /dev/null +++ b/packages/iop_console/lib/src/iop_console_shell.dart @@ -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 createState() => _IopConsoleShellState(); +} + +class _IopConsoleShellState extends State { + 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 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), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/iop_console/pubspec.yaml b/packages/iop_console/pubspec.yaml new file mode 100644 index 0000000..4186819 --- /dev/null +++ b/packages/iop_console/pubspec.yaml @@ -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