diff --git a/README.md b/README.md index 5af0bce..cc30a43 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ NomadCode는 IOP를 사용하는 개발 워크플로우 셸입니다. 사용자 ## 현재 상태 - `services/core`는 health/readiness endpoint, task API, PostgreSQL persistence, goose migration, sqlc query 생성 구조, River job, IOP OpenAI-compatible Responses client 경로, 향후 A2A agent delegation scaffolding, Plane/Mattermost adapter stub을 포함합니다. -- `apps/client`는 NomadCode 제품 UI의 기준 Flutter 앱이며 모바일/데스크톱/선택적 web target, Firebase Messaging, local notifications, HTTP client 의존성을 포함합니다. 현재 top titlebar, 우측 activity rail, 우측 Agent dock panel을 가진 workbench shell scaffold가 들어가 있고, Agent dock은 공통 `agent_shell` package를 사용합니다. +- `apps/client`는 NomadCode 제품 UI의 기준 Flutter 앱이며 모바일/데스크톱/선택적 web target, Firebase Messaging, local notifications, HTTP client 의존성을 포함합니다. 현재 top titlebar, 우측 activity rail, 우측 Agent dock panel을 가진 workbench shell scaffold가 들어가 있고, Agent dock은 공통 `agent_shell` package를 사용합니다. IOP는 우측 rail 하단 아이콘으로 분리되어 있으며, IOP package/widget이 없을 때도 빌드가 깨지지 않는 선택적 `iopContent` slot으로 조립합니다. - `apps/web`은 이전 React/Vite 운영 콘솔 scaffold였으며, Flutter-first 통합에 따라 완전히 폐기 및 제거되었습니다. - `packages/contracts`는 향후 OpenAPI, protobuf, generated client, fixture, compatibility asset을 담을 공유 계약 공간입니다. - 활성 계획과 Milestone 상태는 `agent-roadmap/`에서 관리합니다. README에는 현재 작업 상태를 복사하지 않고 진입 문서만 연결합니다. @@ -20,7 +20,7 @@ NomadCode는 제품과 워크플로우 계층을 소유합니다. - Plane/Jira 스타일 work item intake, provider projection, comment, status update - 사용자에게 노출되는 file change, diff, branch, commit, PR 흐름 - 모바일/데스크톱/선택적 web target의 Flutter project management surface -- top titlebar, 우측 activity rail, 중앙 content 전환, 우측 Agent dock으로 구성되는 workbench shell +- top titlebar, 우측 activity rail, 중앙 content 전환, 우측 Agent dock, 하단 IOP section slot으로 구성되는 workbench shell - IOP 관리 page/tab에 외부 IOP console package 또는 widget을 선택적으로 mount하는 제품 조립 경계 - IOP 호출에 넘길 task metadata 구성 diff --git a/agent-ops/rules/project/domain/mobile/rules.md b/agent-ops/rules/project/domain/mobile/rules.md index a731e05..d30de31 100644 --- a/agent-ops/rules/project/domain/mobile/rules.md +++ b/agent-ops/rules/project/domain/mobile/rules.md @@ -31,7 +31,7 @@ NomadCode의 Flutter-first 제품 UI 도메인이다. 모바일/데스크톱/선 - `lib/main.dart` — Flutter app entrypoint. - `lib/src/app/nomadcode_client_app.dart` — MaterialApp host와 integration lifecycle wiring. -- `lib/src/app/nomadcode_workbench_shell.dart` — top titlebar, 우측 activity rail, 중앙 content 전환, 우측 Agent dock panel을 가진 workbench shell. +- `lib/src/app/nomadcode_workbench_shell.dart` — top titlebar, 우측 activity rail, 중앙 content 전환, 우측 Agent dock panel, 하단 IOP slot을 가진 workbench shell. - `lib/src/features/workspaces/presentation/workspace_home_page.dart` — workspace project/task UI. workbench mount 시 `showAppBar: false`로 내부 app bar를 숨길 수 있다. - `lib/src/integrations/mattermost/mattermost_auth_service.dart` — Mattermost auth flow. - `lib/src/integrations/mattermost/mattermost_push_host_integration.dart` — Mattermost push host initialization, auth handoff, notification navigation. @@ -48,7 +48,8 @@ NomadCode의 Flutter-first 제품 UI 도메인이다. 모바일/데스크톱/선 - Android native push 변경은 Kotlin/Java receiver/service, manifest, Dart bridge 영향을 함께 확인한다. - public API 호출을 추가하면 contracts/core 영향 여부를 확인한다. - project/task/agent/workspace 제품 UI는 Flutter 앱을 기준 구현으로 둔다. -- NomadCode app shell은 top titlebar와 우측 activity rail을 가진 workbench 구조를 기준으로 한다. 우측 Agent dock은 rail에 붙는 panel로 열고 닫으며, 중앙 영역은 workspace/workflow/IOP/web context/settings 같은 product section을 전환한다. +- NomadCode app shell은 top titlebar와 우측 activity rail을 가진 workbench 구조를 기준으로 한다. 우측 Agent dock은 rail에 붙는 panel로 열고 닫으며, 중앙 영역은 workspace/workflow/terminal/review/web context/settings 같은 product section을 전환한다. +- IOP entry는 우측 rail 하단에 둔다. IOP-specific 운영 화면은 `iopContent` 같은 선택적 widget slot으로 mount하고, IOP package가 없는 기본 빌드에서는 placeholder로 유지한다. - 공통 agent interaction UI는 sibling `agent_shell` package를 사용한다. NomadCode-specific workflow/web context capability는 NomadCode mobile domain이 소유하고, IOP-specific 운영 화면은 향후 IOP-owned package/widget을 선택적으로 mount한다. - `flutter test`를 기본 검증으로 사용한다. - 현재 baseline의 `avoid_print` info는 별도 cleanup 작업으로 다루고, 기능 변경과 섞지 않는다. diff --git a/apps/client/README.md b/apps/client/README.md index c925b93..e7fc23d 100644 --- a/apps/client/README.md +++ b/apps/client/README.md @@ -19,9 +19,9 @@ The Flutter host application in this monorepo is standardized under `apps/client ## Workbench Shell -`lib/src/app/nomadcode_workbench_shell.dart` owns the current NomadCode client shell. It provides a top titlebar, a right-side activity rail, center content switching, and an Agent dock panel attached to the right rail. +`lib/src/app/nomadcode_workbench_shell.dart` owns the current NomadCode client shell. It provides a top titlebar, a right-side activity rail, center content switching, and an Agent dock panel attached to the right rail. The rail keeps product/workflow placeholder icons near the top and the IOP entry at the bottom. -The Agent dock uses the sibling `agent_shell` package as a product-neutral chat/agent surface. Workflow, Web Context, and optional IOP management areas are mounted as center content sections. IOP-specific UI should be composed through an IOP-owned package or widget boundary rather than copied into NomadCode client code. +The Agent dock uses the sibling `agent_shell` package as a product-neutral chat/agent surface. Workflow, Web Context, and optional IOP management areas are mounted as center content sections. IOP-specific UI should be composed through an IOP-owned package or widget boundary via the optional `iopContent` slot rather than copied into NomadCode client code. `WorkspaceHomePage` can hide its own app bar through `showAppBar: false` when it is mounted inside the workbench shell. diff --git a/apps/client/lib/src/app/nomadcode_workbench_shell.dart b/apps/client/lib/src/app/nomadcode_workbench_shell.dart index f2a3a20..55dc442 100644 --- a/apps/client/lib/src/app/nomadcode_workbench_shell.dart +++ b/apps/client/lib/src/app/nomadcode_workbench_shell.dart @@ -1,12 +1,25 @@ import 'package:agent_shell/agent_shell.dart'; import 'package:flutter/material.dart'; -enum NomadWorkbenchSection { workspace, workflow, iop, webContext, settings } +enum NomadWorkbenchSection { + workspace, + workflow, + terminal, + review, + webContext, + settings, + iop, +} class NomadCodeWorkbenchShell extends StatefulWidget { final Widget workspace; + final Widget? iopContent; - const NomadCodeWorkbenchShell({super.key, required this.workspace}); + const NomadCodeWorkbenchShell({ + super.key, + required this.workspace, + this.iopContent, + }); @override State createState() => @@ -70,10 +83,17 @@ class _NomadCodeWorkbenchShellState extends State { title: 'Workflow', subtitle: 'Workflow management surface will mount here.', ), - NomadWorkbenchSection.iop => const _WorkbenchPlaceholder( - icon: Icons.hub_outlined, - title: 'IOP', - subtitle: 'Optional IOP console package will mount here when included.', + NomadWorkbenchSection.terminal => const _WorkbenchPlaceholder( + icon: Icons.terminal, + title: 'Terminal', + subtitle: + 'Remote command sessions and terminal bridges will mount here.', + ), + NomadWorkbenchSection.review => const _WorkbenchPlaceholder( + icon: Icons.rate_review_outlined, + title: 'Review', + subtitle: + 'Diff, branch, commit, and PR review surfaces will mount here.', ), NomadWorkbenchSection.webContext => const _WorkbenchPlaceholder( icon: Icons.web_asset_outlined, @@ -85,6 +105,14 @@ class _NomadCodeWorkbenchShellState extends State { title: 'Settings', subtitle: 'Workspace and integration settings will mount here.', ), + NomadWorkbenchSection.iop => + widget.iopContent ?? + const _WorkbenchPlaceholder( + icon: Icons.hub_outlined, + title: 'IOP', + subtitle: + 'Optional IOP console package will mount here when included.', + ), }; } @@ -195,10 +223,16 @@ class _WorkbenchActivityRail extends StatelessWidget { onPressed: () => onSelect(NomadWorkbenchSection.workflow), ), _RailButton( - tooltip: 'IOP', - icon: Icons.hub_outlined, - selected: selected == NomadWorkbenchSection.iop, - onPressed: () => onSelect(NomadWorkbenchSection.iop), + tooltip: 'Terminal', + icon: Icons.terminal, + selected: selected == NomadWorkbenchSection.terminal, + onPressed: () => onSelect(NomadWorkbenchSection.terminal), + ), + _RailButton( + tooltip: 'Review', + icon: Icons.rate_review_outlined, + selected: selected == NomadWorkbenchSection.review, + onPressed: () => onSelect(NomadWorkbenchSection.review), ), _RailButton( tooltip: 'Web Context', @@ -213,6 +247,12 @@ class _WorkbenchActivityRail extends StatelessWidget { selected: selected == NomadWorkbenchSection.settings, onPressed: () => onSelect(NomadWorkbenchSection.settings), ), + _RailButton( + tooltip: 'IOP', + icon: Icons.hub_outlined, + selected: selected == NomadWorkbenchSection.iop, + onPressed: () => onSelect(NomadWorkbenchSection.iop), + ), const SizedBox(height: 8), ], ), @@ -336,8 +376,10 @@ String _sectionLabel(NomadWorkbenchSection section) { return switch (section) { NomadWorkbenchSection.workspace => 'Workspace', NomadWorkbenchSection.workflow => 'Workflow', - NomadWorkbenchSection.iop => 'IOP Management', + NomadWorkbenchSection.terminal => 'Terminal', + NomadWorkbenchSection.review => 'Review', NomadWorkbenchSection.webContext => 'Web Context', NomadWorkbenchSection.settings => 'Settings', + NomadWorkbenchSection.iop => 'IOP Management', }; } diff --git a/apps/client/test/widget_test.dart b/apps/client/test/widget_test.dart index c8ff459..6288e66 100644 --- a/apps/client/test/widget_test.dart +++ b/apps/client/test/widget_test.dart @@ -115,6 +115,24 @@ void main() { expect(find.text('IOP Bridge'), findsOneWidget); }); + testWidgets('Workbench opens agent dock and IOP section from right rail', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(const NomadCodeClientApp()); + + await tester.tap(find.byIcon(Icons.smart_toy_outlined)); + await tester.pumpAndSettle(); + + expect(find.text('Ask the workspace agent'), findsOneWidget); + expect(find.textContaining('NomadCode agent is docked'), findsOneWidget); + + await tester.tap(find.byIcon(Icons.hub_outlined)); + await tester.pumpAndSettle(); + + expect(find.text('IOP'), findsOneWidget); + expect(find.textContaining('Optional IOP console package'), findsOneWidget); + }); + testWidgets('WorkspaceHomePage triggers launcher callback on button tap', ( WidgetTester tester, ) async {