feat: update workbench shell, mobile rules, and tests

This commit is contained in:
toki 2026-05-31 19:36:01 +09:00
parent f769dd5f3c
commit d4ff947dcd
5 changed files with 78 additions and 17 deletions

View file

@ -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을 포함합니다. - `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 통합에 따라 완전히 폐기 및 제거되었습니다. - `apps/web`은 이전 React/Vite 운영 콘솔 scaffold였으며, Flutter-first 통합에 따라 완전히 폐기 및 제거되었습니다.
- `packages/contracts`는 향후 OpenAPI, protobuf, generated client, fixture, compatibility asset을 담을 공유 계약 공간입니다. - `packages/contracts`는 향후 OpenAPI, protobuf, generated client, fixture, compatibility asset을 담을 공유 계약 공간입니다.
- 활성 계획과 Milestone 상태는 `agent-roadmap/`에서 관리합니다. README에는 현재 작업 상태를 복사하지 않고 진입 문서만 연결합니다. - 활성 계획과 Milestone 상태는 `agent-roadmap/`에서 관리합니다. README에는 현재 작업 상태를 복사하지 않고 진입 문서만 연결합니다.
@ -20,7 +20,7 @@ NomadCode는 제품과 워크플로우 계층을 소유합니다.
- Plane/Jira 스타일 work item intake, provider projection, comment, status update - Plane/Jira 스타일 work item intake, provider projection, comment, status update
- 사용자에게 노출되는 file change, diff, branch, commit, PR 흐름 - 사용자에게 노출되는 file change, diff, branch, commit, PR 흐름
- 모바일/데스크톱/선택적 web target의 Flutter project management surface - 모바일/데스크톱/선택적 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 관리 page/tab에 외부 IOP console package 또는 widget을 선택적으로 mount하는 제품 조립 경계
- IOP 호출에 넘길 task metadata 구성 - IOP 호출에 넘길 task metadata 구성

View file

@ -31,7 +31,7 @@ NomadCode의 Flutter-first 제품 UI 도메인이다. 모바일/데스크톱/선
- `lib/main.dart` — Flutter app entrypoint. - `lib/main.dart` — Flutter app entrypoint.
- `lib/src/app/nomadcode_client_app.dart` — MaterialApp host와 integration lifecycle wiring. - `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/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_auth_service.dart` — Mattermost auth flow.
- `lib/src/integrations/mattermost/mattermost_push_host_integration.dart` — Mattermost push host initialization, auth handoff, notification navigation. - `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 영향을 함께 확인한다. - Android native push 변경은 Kotlin/Java receiver/service, manifest, Dart bridge 영향을 함께 확인한다.
- public API 호출을 추가하면 contracts/core 영향 여부를 확인한다. - public API 호출을 추가하면 contracts/core 영향 여부를 확인한다.
- project/task/agent/workspace 제품 UI는 Flutter 앱을 기준 구현으로 둔다. - 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한다. - 공통 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`를 기본 검증으로 사용한다. - `flutter test`를 기본 검증으로 사용한다.
- 현재 baseline의 `avoid_print` info는 별도 cleanup 작업으로 다루고, 기능 변경과 섞지 않는다. - 현재 baseline의 `avoid_print` info는 별도 cleanup 작업으로 다루고, 기능 변경과 섞지 않는다.

View file

@ -19,9 +19,9 @@ The Flutter host application in this monorepo is standardized under `apps/client
## Workbench Shell ## 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. `WorkspaceHomePage` can hide its own app bar through `showAppBar: false` when it is mounted inside the workbench shell.

View file

@ -1,12 +1,25 @@
import 'package:agent_shell/agent_shell.dart'; import 'package:agent_shell/agent_shell.dart';
import 'package:flutter/material.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 { class NomadCodeWorkbenchShell extends StatefulWidget {
final Widget workspace; final Widget workspace;
final Widget? iopContent;
const NomadCodeWorkbenchShell({super.key, required this.workspace}); const NomadCodeWorkbenchShell({
super.key,
required this.workspace,
this.iopContent,
});
@override @override
State<NomadCodeWorkbenchShell> createState() => State<NomadCodeWorkbenchShell> createState() =>
@ -70,10 +83,17 @@ class _NomadCodeWorkbenchShellState extends State<NomadCodeWorkbenchShell> {
title: 'Workflow', title: 'Workflow',
subtitle: 'Workflow management surface will mount here.', subtitle: 'Workflow management surface will mount here.',
), ),
NomadWorkbenchSection.iop => const _WorkbenchPlaceholder( NomadWorkbenchSection.terminal => const _WorkbenchPlaceholder(
icon: Icons.hub_outlined, icon: Icons.terminal,
title: 'IOP', title: 'Terminal',
subtitle: 'Optional IOP console package will mount here when included.', 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( NomadWorkbenchSection.webContext => const _WorkbenchPlaceholder(
icon: Icons.web_asset_outlined, icon: Icons.web_asset_outlined,
@ -85,6 +105,14 @@ class _NomadCodeWorkbenchShellState extends State<NomadCodeWorkbenchShell> {
title: 'Settings', title: 'Settings',
subtitle: 'Workspace and integration settings will mount here.', 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), onPressed: () => onSelect(NomadWorkbenchSection.workflow),
), ),
_RailButton( _RailButton(
tooltip: 'IOP', tooltip: 'Terminal',
icon: Icons.hub_outlined, icon: Icons.terminal,
selected: selected == NomadWorkbenchSection.iop, selected: selected == NomadWorkbenchSection.terminal,
onPressed: () => onSelect(NomadWorkbenchSection.iop), onPressed: () => onSelect(NomadWorkbenchSection.terminal),
),
_RailButton(
tooltip: 'Review',
icon: Icons.rate_review_outlined,
selected: selected == NomadWorkbenchSection.review,
onPressed: () => onSelect(NomadWorkbenchSection.review),
), ),
_RailButton( _RailButton(
tooltip: 'Web Context', tooltip: 'Web Context',
@ -213,6 +247,12 @@ class _WorkbenchActivityRail extends StatelessWidget {
selected: selected == NomadWorkbenchSection.settings, selected: selected == NomadWorkbenchSection.settings,
onPressed: () => onSelect(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), const SizedBox(height: 8),
], ],
), ),
@ -336,8 +376,10 @@ String _sectionLabel(NomadWorkbenchSection section) {
return switch (section) { return switch (section) {
NomadWorkbenchSection.workspace => 'Workspace', NomadWorkbenchSection.workspace => 'Workspace',
NomadWorkbenchSection.workflow => 'Workflow', NomadWorkbenchSection.workflow => 'Workflow',
NomadWorkbenchSection.iop => 'IOP Management', NomadWorkbenchSection.terminal => 'Terminal',
NomadWorkbenchSection.review => 'Review',
NomadWorkbenchSection.webContext => 'Web Context', NomadWorkbenchSection.webContext => 'Web Context',
NomadWorkbenchSection.settings => 'Settings', NomadWorkbenchSection.settings => 'Settings',
NomadWorkbenchSection.iop => 'IOP Management',
}; };
} }

View file

@ -115,6 +115,24 @@ void main() {
expect(find.text('IOP Bridge'), findsOneWidget); 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', ( testWidgets('WorkspaceHomePage triggers launcher callback on button tap', (
WidgetTester tester, WidgetTester tester,
) async { ) async {