대시보드
Morit Plugin3. 화면과 사용자 경험

UI Runtime v2 레퍼런스

UI Runtime v2는 서명된 .mplg가 화면, 상태, data source, action을 JSON으로 선언하는 Host-rendered 계약입니다. Flutter/Dart, JavaScript, HTML, CSS, WebView, native code를 UI로 실행하지 않습니다.

Config 구조

json
{
  "ui_schema": 2,
  "icon": "calendar",
  "description": "오늘과 이번 주 학교 생활을 확인합니다.",
  "theme": {
    "density": "standard",
    "radius": 18,
    "spacing": 12
  },
  "app_bar": {
    "title": "학교 생활",
    "subtitle": "{{data.today.summary}}",
    "pinned": true,
    "actions": [
      {
        "id": "settings",
        "label": "설정",
        "icon": "settings",
        "show_as": "overflow",
        "action": {"type": "navigate", "target": "school_life.settings"}
      }
    ]
  },
  "navigation": {
    "type": "adaptive",
    "selected_state_key": "section",
    "persist": true,
    "label_behavior": "selected",
    "items": [
      {
        "id": "today",
        "label": "오늘",
        "icon": "calendar",
        "value": "today",
        "action": {"type": "set_state", "values": {"section": "today"}}
      },
      {
        "id": "week",
        "label": "이번 주",
        "icon": "clock",
        "value": "week",
        "action": {"type": "navigate", "target": "school_life.week"}
      }
    ]
  },
  "initial_state": {"section": "today"},
  "data_sources": [
    {
      "id": "today",
      "capability": "school_life.today",
      "trigger": "load",
      "query": null,
      "arguments": {},
      "refresh_seconds": 300
    }
  ],
  "view": {
    "type": "column",
    "props": {"spacing": 12},
    "children": [
      {"type": "text", "props": {"text": "{{data.today.summary}}", "style": "heading"}}
    ]
  }
}

허용 config 필드는 ui_schema, icon, description, placement, app_bar, navigation, theme, initial_state, data_sources, a2ui, view, bottom_bar입니다. a2ui는 point: "response"에서만 사용합니다. bottom_bar는 min_morit_version: "1.7.14" 이상이 필요합니다(아래 “SDK 1.7.14: 하단 실행 영역과 페이지 목록” 참고).

트리와 크기 제한

  • tree 최대 160 node, depth 12(root가 depth 0), node당 child 최대 32개
  • data source 최대 8개
  • state key 최대 32개
  • config JSON 최대 32 KiB
  • 자동 refresh 30~86,400초
  • 한 extension의 app bar action 최대 6개
  • navigation item 2~8개
  • binding 깊이 최대 12 segment
  • JSON binding 값 깊이 최대 8, 배열·객체 항목 최대 64개

positioned, scroll, padding, center, expanded, badge는 child를 정확히 하나만 가집니다. surface는 child가 하나 이상이어야 합니다. positioned는 stack의 직접 child이고 left, top, right, bottom 중 하나 이상을 지정해야 합니다. expanded는 row 또는 column의 직접 child입니다.

크기 prop은 04096, position offset은 -40964096, spacing과 inset은 0128입니다. border_width 08, border_radius 064, elevation 024, opacity 01, aspect_ratio 0.120, flex 1~24를 사용합니다. padding과 margin의 정확한 입력 형태와 theme 색상 역할은 토큰과 반응형에 있습니다.

Plugin Theme

theme를 생략한 기존 플러그인은 Morit의 현재 Material 3 라이트·다크 theme를 그대로 상속합니다. 선택적으로 base와 mode별 일부 값만 재정의할 수 있고 누락 값은 항상 Host 값으로 채웁니다.

json
{
  "theme": {
    "radius": 18,
    "typography": {"scale": 1.0, "body_weight": 400, "title_weight": 700},
    "surface": {"elevation": 1},
    "border": {"width": 1, "radius": 18},
    "icon": {"size": 22},
    "states": {"disabled_opacity": 0.38},
    "light": {
      "color_scheme": {
        "primary": "#315DA8",
        "on_primary": "#FFFFFF",
        "surface": "#F9F9FF",
        "on_surface": "#1A1B20"
      }
    },
    "dark": {
      "color_scheme": {
        "primary": "#AFC6FF",
        "on_primary": "#002F66",
        "surface": "#111318",
        "on_surface": "#E2E2E9"
      },
      "states": {"selected_color": "primary_container"}
    }
  }
}

지원 그룹은 color_scheme, typography, surface, border, icon, states, radius, spacing, density와 최상위 light, dark입니다. mode variant 안에 다시 light/dark를 중첩할 수 없습니다. 명백히 같은 literal 전경/배경은 validate 오류이고, 계산 가능한 대비가 4.5:1보다 낮으면 경고합니다. semantic token이나 Host 상속 때문에 정적으로 확정할 수 없는 조합은 Preview와 실제 앱에서 확인합니다.

State

initial_state가 허용 state key와 초기값을 선언합니다. key는 영문자로 시작하고 영문·숫자·밑줄을 사용하는 최대 64자입니다.

json
{
  "initial_state": {"period": "today", "notifications": true}
}

field, select, switch, navigation selection, set_state는 여기에 선언된 key만 변경합니다. Host는 focus된 입력과 TextEditingController를 action 실행 직전에 commit하므로 조합 중인 한국어 IME 문자도 Tool arguments와 validation에 최신 값으로 반영됩니다. persist: true는 Instance settings의 extension 전용 namespace에 저장합니다. credential과 민감한 사용자 데이터는 state에 저장하지 않습니다.

입력과 검증

  • field.suggestions는 정적 autocomplete 문자열, suggestions_source는 data.<source>... 배열입니다.
  • select.options는 정적 {value,label} 목록, options_source는 capability가 돌려준 비동기 목록입니다.
  • select.searchable: true는 Material 3 검색 select를 사용합니다.
  • required, min_length, max_length, minimum, maximum, error_text는 field/select/switch 검증에 사용합니다. action의 validate: true는 잘못된 form을 실행하지 않습니다.

원격 자동완성은 입력 state를 data source의 query 또는 arguments에 연결합니다. 두 글자부터 350ms 후 요청하며, 새 입력은 이전 요청의 결과를 무효화합니다. 결과가 도착하면 추가 입력 없이 목록을 표시하고 입력 포커스를 유지합니다. 키보드 완료는 표시된 후보를 선택하고 field action을 한 번 실행합니다. 후보가 없으면 현재 입력으로 action을 실행합니다.

json
{
  "initial_state": {"query": ""},
  "data_sources": [{
    "id": "search",
    "capability": "catalog.search",
    "trigger": "manual",
    "query": "{{state.query}}"
  }],
  "view": {
    "type": "field",
    "props": {
      "state_key": "query",
      "label": "검색",
      "suggestions_source": "data.search.data.items"
    }
  }
}

목록은 문자열 또는 label, title, name, value 중 하나로 표시 이름을 제공하는 객체를 받습니다. 선택 값은 표시 문자열입니다. 객체 ID를 저장해야 한다면 select.options_source를 사용합니다. 로딩·재시도 중에는 이전 후보를 선택할 수 없으며, 앱 복귀 시 진행 중이던 검색을 다시 요청합니다. 동일 요청의 캐시는 실제 요청 인수에 연결됩니다.

Data source와 binding

json
{
  "id": "schedule",
  "capability": "school_life.schedule",
  "trigger": "load",
  "query": "{{state.period}}",
  "arguments": {"period": "{{state.period}}"},
  "refresh_seconds": 300
}

trigger는 load 또는 manual입니다. 결과는 다음 namespace로 읽습니다.

text
data.<source_id>.completed
data.<source_id>.summary
data.<source_id>.data.<field>

반복 template 안에서는 item.<field>를 사용합니다. 문자열 전체가 binding 하나면 원래 JSON type을 arguments에 전달하고, 다른 글자와 섞으면 텍스트 template이 됩니다.

json
{"limit": "{{state.limit}}"}
json
{"label": "{{state.period}} 일정"}

context.platform, context.screen.width|height|size_class, context.permissions.<permission>, context.connections.<credential>, context.connected, context.loading.<source>, context.errors.<source>, context.route.<parameter>도 조건과 binding에 사용할 수 있습니다.

Capability의 input_schema/output_schema는 안전한 JSON Schema 부분집합을 사용합니다. validate는 data source와 invoke arguments를 input schema와, data.<source>.data... binding을 output schema와 정적으로 대조합니다. 필수 필드·타입·존재하지 않는 경로는 package build 전에 구체적인 오류로 거부됩니다.

Local Storage binding

저장소도 같은 data source/action 흐름을 사용합니다.

json
{
  "id": "tasks",
  "capability": "morit.storage.list",
  "trigger": "load",
  "query": "",
  "arguments": {"namespace": "tasks", "limit": 20}
}

list.source에는 data.tasks.data.items를 사용합니다. Storage mutation이나 AI 응답 완료 뒤 Host가 Storage source만 갱신하며 앱 재개 시에도 최신 값을 읽습니다. CRUD, 권한과 migration은 Plugin Local Storage와 AI 접근을 참고하세요.

Capability 결과 이미지

image와 avatar의 url은 literal URL이 아니라 capability 결과를 가리키는 전체 {{data.<source>...}} 또는 {{item...}} binding입니다. Manifest가 network를 요청하고 활성 Instance가 network를 grant한 경우에만 표시할 수 있습니다.

Host renderer는 해석된 URL을 앱에서 직접 fetch하지 않고 인증된 POST /v1/plugin-instances/{instance_id}/ui-images:fetch를 사용합니다. 요청 body는 다음과 같습니다.

json
{"url": "https://images.example.edu/today.png"}

Host proxy는 공개 HTTPS와 SSRF 정책을 적용하고 redirect를 따르지 않습니다. PNG/JPEG/GIF/WebP만 허용하며 응답 MIME type, magic byte, 실제 이미지 포맷이 모두 일치해야 합니다. 응답 본문은 최대 512 KiB, 가로·세로는 각각 최대 4096 px입니다. 정적 이미지는 1 frame, 애니메이션은 최대 128 frame이며 width × height × frame 수로 계산한 frame 합산 pixel은 최대 16,000,000입니다. 검증 실패는 해당 이미지 fallback으로 격리하며 원본 URL이나 실패 응답을 직접 렌더링하지 않습니다.

App bar

app_bar는 Host 화면 chrome을 구성합니다. tree 안에 앱 바를 다시 만들지 않습니다.

필드규칙
title필수, binding 가능, 최대 120자
subtitle선택, binding 가능, 최대 160자
center_title, pinnedboolean
leadingaction item 하나
actions최대 6개

action item은 id, label, 선택적 icon, show_as, 필수 action을 가집니다. show_as는 auto, always, overflow입니다. overflow에는 드문 행동을 두고 주요 행동은 화면 본문에 둡니다.

Navigation

navigation.type은 tabs, bar, rail, drawer, adaptive입니다. 각 item은 id, label, 선택적 icon, selected_icon, value, 필수 action을 가집니다.

selected_state_key를 사용하면 해당 key가 initial_state에 있어야 하고 모든 item에 value가 필요합니다. label_behavior는 auto, always, selected, never입니다. rail_breakpoint는 480~1600입니다.

Component tree

지원 node:

text
column row wrap grid stack positioned scroll padding center expanded
surface card section text icon image avatar badge divider spacer
button chip metric progress list timeline calendar chart
form dialog sheet field select switch empty

각 node의 목적과 props는 컴포넌트 레퍼런스, 색·크기 범위는 토큰과 반응형을 참고하세요.

Action

Capability 실행

json
{
  "type": "invoke",
  "capability": "school_life.lookup",
  "query": "{{state.query}}",
  "arguments": {"limit": 10},
  "store": "results"
}

store는 선언된 data source ID이며 성공 결과로 해당 source를 교체합니다.

Action Flow와 Form

json
{
  "type": "flow",
  "mode": "sequential",
  "validate": true,
  "continue_on_error": false,
  "actions": [
    {"type": "invoke", "capability": "school.save", "arguments": {"name": "{{state.name}}"}, "store": "saved"},
    {"type": "navigate", "target": "school.details", "parameters": {"id": "{{data.saved.data.id}}"}, "result_state": "route_result"}
  ]
}

mode는 sequential 또는 parallel입니다. flow는 최대 4 depth, 전체 16 action, flow당 8 action으로 제한됩니다. 순차 flow는 앞 action의 store를 다음 action에서 즉시 읽고, 병렬 flow는 서로 의존하지 않는 action을 함께 실행합니다.

State와 refresh

json
{"type": "set_state", "values": {"period": "tomorrow"}, "persist": true}
json
{"type": "refresh", "source": "schedule"}

Navigation

json
{
  "type": "navigate",
  "target": "school_life.details",
  "parameters": {"date": "{{state.date}}"},
  "result_state": "details_result",
  "transition": "platform",
  "replace": false
}

transition은 platform, fade, slide, none입니다. replace: true는 현재 route를 뒤로가기 stack에서 교체합니다. 같은 manifest의 UI extension ID만 target이 될 수 있습니다. parameters는 대상의 context.route.<parameter>로 전달되고 닫힌 결과는 result_state에 지정한 initial_state key에 반영됩니다. 대상은 back.result로 결과 객체를 반환합니다.

json
{"type": "back"}

조건부 표시

json
{
  "visible_when": {
    "path": "data.schedule.data.items",
    "exists": true
  }
}

단일 조건은 equals, not_equals, exists 중 정확히 하나를 사용합니다. all, any, not으로 조건을 재귀적으로 조합할 수 있으며 조건 depth 4, 전체 32개로 제한됩니다. 조건은 state, data, item, context만 읽고 임의 expression을 실행하지 않습니다.

로딩과 오류 복구

Host는 source별 실행을 격리하고 중복 실행을 합칩니다. 화면이 background로 가면 자동 refresh를 멈추고 다시 foreground가 될 때 최신 source를 확인합니다. 갱신 실패 시 기존 정상 데이터가 있으면 유지하고, 첫 로드도 실패한 경우에만 해당 영역에 retry 상태를 표시합니다. package generation이나 화면이 바뀐 뒤 도착한 오래된 응답은 버립니다.

Host는 Instance·capability·해석된 query/arguments가 같은 결과를 Shell 수명의 공유 cache에 보관합니다. 홈, 플러그인 화면, AI Response UI에서 같은 source를 열면 추가 호출 없이 즉시 재사용하고 mutation/refresh 결과를 구독 중인 모든 surface에 자동 전파합니다. 알림 Tool은 같은 Instance local storage를 source of truth로 읽으며 알림 action의 storage mutation도 storage revision을 갱신해 열린 UI source를 자동 refresh합니다.

Runtime Inspector

Preview와 debug Host의 Runtime Inspector는 현재 state, data source 결과, context, loading/error, 마지막 action과 검증 오류를 표시합니다. secret과 credential 값은 포함하지 않으며 배포 build의 일반 사용자 화면에서는 노출하지 않습니다.

Response UI

point: "response"도 같은 theme, app_bar, navigation, data/state/node/action 계약을 재사용합니다. 추가로 a2ui.version, 사용 조건을 설명하는 a2ui.description, 실제 Tool 결과를 검증할 a2ui.schema가 필수입니다. 결과는 AI가 catalog 후보를 명시적으로 선택하고 Host의 유효성·관련성·중복·권한 검증을 통과했을 때만 해당 AI 메시지의 한 response container 안에 렌더링됩니다. 서버는 response를 일괄 생성하거나 shape로 추측하지 않습니다. 상세 계약과 기본 catalog는 Response UI와 Agent Timeline을 참고하세요.

Preview와 실제 Host

morit plugin preview . --output ./dist/preview.html 또는 morit_project_preview는 validate를 먼저 통과한 config를 실행 코드 없이 안전한 HTML로 렌더링합니다. Preview는 Flutter Host와 같은 binding, compound condition, state commit, form validation, autocomplete/select, 순차·병렬 flow, route params/result 규칙을 적용하고 invoke/navigation을 검증 가능한 Host event로 노출합니다. 상단 토글로 light/dark variant와 Host 상속 결과를 비교하고 Runtime Inspector로 상태를 확인합니다. 실제 인증·권한·network Tool과 native route stack만 서명 package를 앱에 설치해 최종 확인합니다.

1.7.12 호환성

기존 UI v2 manifest는 변경 없이 동작합니다. compound condition, action flow, input 검증, async options, route data, capability schema는 모두 additive입니다. 기존 단일 action과 {path, equals|not_equals|exists} 조건은 그대로 유지됩니다. 새 필드를 사용하는 package는 min_morit_version: "1.7.12"를 지정합니다.

SDK 1.7.13의 갱신·캐시 동작

데이터만 갱신되면 작성 중인 필드와 포커스를 유지합니다. 다른 route parameters로 이동하면 이전 요청을 무효화하고 새 경로의 데이터를 불러옵니다. 연결·권한 컨텍스트 변경 시에는 입력을 보존하면서 이전 데이터 요청을 무효화합니다.

공유 데이터 캐시는 설치 인스턴스, 패키지 버전, 연결·권한 컨텍스트와 실제 요청을 구분합니다. 만료는 60초, 상한은 128개 또는 직렬화된 키·결과 기준 8MiB이며 최근 사용한 순서로 보관합니다. 동시에 같은 데이터를 요청하는 화면은 한 요청을 공유합니다. 화면이 사라지거나 요청이 대체된 경우 늦은 결과로 화면을 다시 채우지 않습니다. 캐시는 영구 저장소가 아닙니다.

SDK 1.7.14: 하단 실행 영역과 페이지 목록

새 기능을 사용하는 manifest에는 min_morit_version: "1.7.14" 이상을 지정합니다. 기존 1.7.13 UI의 색상·카드·레이아웃 계약은 유지됩니다.

bottom_bar는 config의 view와 같은 수준에 놓는 UI 노드입니다. 전체 화면에서는 본문만 스크롤되고 하단 행동은 키보드 위에 유지됩니다. 큰 글자와 작은 가로 화면에서는 하단 영역 자체도 스크롤할 수 있습니다. 임베디드 화면에서는 본문 바로 뒤에 표시합니다. 권한·action·binding·asset 검증은 본문과 동일하며 두 영역 합계 160개 노드 한도를 적용합니다.

json
{
  "ui_schema": 2,
  "initial_state": {"page": 0},
  "data_sources": [{"id": "results", "capability": "notes.search", "trigger": "manual"}],
  "view": {
    "id": "results",
    "type": "grid",
    "props": {
      "source": "data.results.data.items",
      "page_size": 20,
      "page_state_key": "page",
      "item_key": "id",
      "columns": 2,
      "min_item_width": 148,
      "height": 400,
      "item_height": 160,
      "spacing": 12,
      "empty_text": "검색 결과가 없어요."
    },
    "children": [{"type": "card", "props": {"title": "{{item.title}}"}}]
  },
  "bottom_bar": {
    "type": "button",
    "props": {"label": "검색", "icon": "search", "full_width": true},
    "action": {"type": "refresh", "source": "results"}
  }
}
  • page_size: 1–100. page_state_key와 함께 사용합니다. 이 키는 initial_state에 선언하며 0부터 시작합니다.
  • 페이지는 이미 받은 배열을 나누어 표시합니다. 최대 10,000개까지 탐색하며 서버 API를 자동으로 페이지 조회하지 않습니다. 서버 cursor/page는 기존 data source의 arguments와 set_state → refresh flow로 연결합니다.
  • list에 height를 주면 세로 viewport 안의 항목만 생성합니다. grid에는 source와 자식 템플릿 하나를 지정합니다.
  • 데이터 grid에 height와 item_height(48–4096)를 함께 주면 지연 생성합니다. item_height는 글자 확대에 맞춰 증가합니다. 본문 길이가 제각각인 카드에는 고정 높이를 생략하고 작은 페이지를 사용합니다.
  • item_key에는 결과 안의 안정적이고 고유한 id를 지정합니다. 페이지 선택·스크롤은 기존 화면 상태 복원 규칙을 따릅니다.
  • page_size를 생략하면 기존 limit(기본 20, 최대 100) 동작을 유지하며 페이지 버튼은 나타나지 않습니다.

이미지 위 제목·badge·행동 배치는 기존 stack + positioned + image 조합으로 구성합니다. 텍스트와 아이콘의 대비, 터치 영역, 이미지 실패 상태를 확인하고 새 전용 overlay 컴포넌트를 만들 필요는 없습니다.

속성 진단

공개 SDK contract의 ui_runtime.common_props와 component_props가 컴포넌트별 적용 속성을 제공합니다. 기존에 허용되었지만 해당 컴포넌트가 쓰지 않는 속성은 설치를 막지 않고 CLI 검증·MCP 결과의 warnings 및 앱 Runtime Inspector에 경고합니다. 예: view.children[0].props.spacing: ignored by text. 범위·유형이 잘못된 속성은 위치를 포함한 오류로 거부합니다. 새 페이지 속성을 무관한 컴포넌트에 넣으면 오류입니다. 브라우저 Preview에서도 하단 영역과 페이지 전환을 확인할 수 있으나 최종 판정은 Flutter Host에서 합니다.