대시보드
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입니다. a2uipoint: "response"에서만 사용합니다.

트리와 크기 제한

  • 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가 하나 이상이어야 합니다. positionedstack의 직접 child이고 left, top, right, bottom 중 하나 이상을 지정해야 합니다. expandedrow 또는 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를 사용합니다. paddingmargin의 정확한 입력 형태와 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_sourcedata.<source>... 배열입니다.
  • select.options는 정적 {value,label} 목록, options_source는 capability가 돌려준 비동기 목록입니다.
  • select.searchable: true는 Material 3 검색 select를 사용합니다.
  • required, min_length, max_length, minimum, maximum, error_textfield/select/switch 검증에 사용합니다. action의 validate: true는 잘못된 form을 실행하지 않습니다.

Data source와 binding

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

triggerload 또는 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 결과 이미지

imageavatarurl은 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_asauto, always, overflow입니다. overflow에는 드문 행동을 두고 주요 행동은 화면 본문에 둡니다.

Navigation

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

selected_state_key를 사용하면 해당 key가 initial_state에 있어야 하고 모든 item에 value가 필요합니다. label_behaviorauto, 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"}
  ]
}

modesequential 또는 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
}

transitionplatform, 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"를 지정합니다.