익스클루시브
Agent Lenses <mv-agent-lenses>
백그라운드 AI 에이전트를 하나의 모델과 다섯 가지 렌즈(Board, Inbox, Tasks, Calendar, Graph)로 보여 줍니다. 각 에이전트는 레이아웃 사이를 실제로 이동하는 하나의 지속적인 카드입니다. 렌즈를 바꾸면 각 카드가 FLIP으로 이전 위치에서 새 위치로 옮겨 가면서 전체 카드에서 행, 타임 바, 그래프 노드로 모양을 바꾸므로 어떤 에이전트도 놓치지 않습니다.
작동 방식
백그라운드 AI 에이전트의 단일 모델, 다섯 가지 렌즈(Board, Inbox, Tasks, Calendar, Graph). 각 에이전트는 레이아웃 사이를 물리적으로 이동하는 하나의 지속적인 카드입니다. 렌즈를 전환하면 각 카드가 FLIP으로 이전 위치에서 새 위치로 이동하면서 전체 카드에서 행, 시간 막대 또는 그래프 노드로 모양을 바꾸므로 에이전트를 놓칠 일이 없습니다. 각 렌즈는 다른 렌즈의 사각지대를 보완합니다: 보드의 드롭 의도 표시(“Drop here = restart the agent”), 인박스에서 묶여 표시되는 의존성 체인, 접힌 작업의 하위 상태 요약, 캘린더에서 서서히 사라지는 종료 시점 없는 실행, 의존성 그래프에서 실시간으로 흐르는 엣지. 실시간 진행률, 경과 시간, 차단된 에이전트에 대한 빠른 답장도 제공합니다.
| 카테고리 | 데이터 표시 |
|---|---|
| 유형 | Web Component (<mv-agent-lenses>) |
| 상태 | 안정 |
| 키트 | 검증할 수 있는 AI |
| Keywords | exclusive, ai, agents, agent-management, kanban, inbox, tree, gantt, timeline, dag, dependency-graph, flip, morph, multi-view, live, human-in-the-loop |
When to use
- An app manages several background AI agents and users need board, inbox, task, calendar and graph views
- Blocked agents need quick replies and dependencies between agents must stay visible
- Switching views must keep each agent traceable as its card moves to its new place
Avoid when
- The work items are ordinary tasks moved across columns, not live agents → use Kanban instead
- A single long-running AI job needs terms, a budget cap and checkpoints → use Run Contract instead
- Only one or two agents run at a time; five lenses add more interface than the task needs
설치
node scripts/add.mjs agent-lenses --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["agent-lenses"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dismiss.js, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, core/position.js, components/agent-lenses/agent-lenses.js, components/agent-lenses/agent-lenses.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-agent-lenses label="Fall launch">
<script type="application/json">[
{ "id": "copy", "name": "Write launch copy", "status": "running", "progress": 0.6 },
{ "id": "images", "name": "Resize hero images", "status": "blocked", "needsAction": "Use the new logo?", "replies": ["Yes", "No"] },
{ "id": "posts", "name": "Schedule social posts", "status": "scheduled", "dependsOn": ["copy", "images"] }
]</script>
</mv-agent-lenses>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="al-demo" style="width:100%;max-width:78rem;margin-inline:auto">
<style>
#al-demo .al-head { display:flex; align-items:flex-end; justify-content:space-between; gap:.5rem 1.5rem; flex-wrap:wrap; margin:0 0 .75rem }
#al-demo .al-head h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
#al-demo .al-head p { margin:.2rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
#al-demo .al-hint { color:var(--mv-fg-muted); font-size:.75rem; line-height:1.9 }
#al-demo mv-agent-lenses { --mv-agent-lenses-max-height: 36rem }
#al-demo .al-log { display:grid; gap:.25rem; margin:.75rem 0 0; padding:0; list-style:none; font-size:.75rem; color:var(--mv-fg-muted) }
#al-demo .al-log li { display:flex; gap:.5rem; align-items:baseline; min-width:0 }
#al-demo .al-log code { flex:none; padding:.0625rem .375rem; border-radius:var(--mv-radius-sm); background:var(--mv-bg-muted); color:var(--mv-fg); font:500 .6875rem/1.4 var(--mv-font-mono) }
#al-demo .al-log span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
</style>
<div class="al-head">
<div>
<h3>Fall collection launch</h3>
<p>Fieldnote Outfitters · 11 agents and subagents working in the background</p>
</div>
<span class="al-hint">Drag a board card, or press <kbd class="mv-kbd">Alt</kbd> <kbd class="mv-kbd">←</kbd> <kbd class="mv-kbd">→</kbd> on a focused card</span>
</div>
<mv-agent-lenses id="al-demo-lenses" label="Fall launch agents" lens="board"></mv-agent-lenses>
<ol class="al-log" id="al-demo-log" aria-label="Event log"></ol>
<script type="module">
const el = document.getElementById("al-demo-lenses");
const logEl = document.getElementById("al-demo-log");
const at = (min) => Date.now() + min * 60000;
// Set before the element is defined: the component picks it up on upgrade.
el.agents = [
{ id: "research", name: "Competitor research", goal: "Review the fall lines of six competing brands: pricing, materials, colorways.", status: "reviewed", start: at(-190), end: at(-122), updated: at(-96) },
{ id: "prices", name: "Price watch", goal: "Check competitor prices every hour and flag any gap above 10%.", status: "running", start: at(-172), trigger: "On every competitor price drop", updated: at(-9) },
{ id: "copy", name: "Product copywriting", goal: "Write the 48 product pages of the collection from the brief and the research.", status: "running", dependsOn: ["research"], start: at(-112), eta: at(58), progress: 0.58, updated: at(-3) },
{ id: "copy-coats", name: "Coats & jackets copy", parent: "copy", goal: "16 pages: fit, materials, care, size guide.", status: "done", start: at(-108), end: at(-41), updated: at(-41) },
{ id: "copy-knit", name: "Knitwear copy", parent: "copy", goal: "18 sweater and cardigan pages, highlighting recycled wool.", status: "running", start: at(-62), eta: at(24), progress: 0.66, updated: at(-2) },
{ id: "copy-acc", name: "Accessories copy", parent: "copy", goal: "14 pages for beanies, scarves and gloves.", status: "scheduled", start: at(22), eta: at(58), updated: at(-30) },
{ id: "translate", name: "German & Italian translation", goal: "Translate and localize the product pages for the Berlin and Milan stores.", status: "scheduled", dependsOn: ["copy"], eta: at(150), updated: at(-52) },
{ id: "visuals", name: "Campaign visuals", goal: "Homepage banner, six social posts and a 20-page lookbook.", status: "running", dependsOn: ["research"], start: at(-84), eta: at(72), progress: 0.41, updated: at(-6) },
{ id: "retouch", name: "Packshot retouching", parent: "visuals", goal: "Cutouts and color matching for 24 packshots.", status: "done", start: at(-78), end: at(-13), updated: at(-13),
needsAction: "All 24 retouched packshots are ready. Publish them to the store?", replies: ["Approve", "Request changes"] },
{ id: "newsletter", name: "Newsletter scheduling", goal: "Schedule the collection announcement for 42,000 subscribers.", status: "blocked", dependsOn: ["research"], start: at(-38), eta: at(35), updated: at(-19),
needsAction: "Send the newsletter on Thursday, Oct 2 at 9:00 AM?", replies: ["Approve", "Suggest another time"] },
{ id: "golive", name: "Store go-live", goal: "Publish the collection on all three storefronts and switch on the homepage banner.", status: "scheduled", dependsOn: ["translate", "visuals"], eta: at(205), updated: at(-52) },
];
const name = (id) => el.agents.find((a) => a.id === id)?.name ?? id;
const log = (type, text) => {
const li = document.createElement("li");
const code = document.createElement("code");
code.textContent = type;
const span = document.createElement("span");
span.textContent = text;
li.append(code, span);
logEl.prepend(li);
while (logEl.children.length > 3) logEl.lastElementChild.remove();
};
el.addEventListener("mv-lens-change", (e) => log("mv-lens-change", `${e.detail.previous} → ${e.detail.lens}`));
el.addEventListener("mv-agent-select", (e) => log("mv-agent-select", e.detail.agent.name));
el.addEventListener("mv-agent-move", (e) => log("mv-agent-move", `${name(e.detail.id)}: ${e.detail.from} → ${e.detail.to} (${e.detail.meaning})`));
el.addEventListener("mv-agent-reply", (e) => {
const { id, reply } = e.detail;
log("mv-agent-reply", `${name(id)}: “${reply}”`);
// The host decides what a reply does; here, a plausible agent reaction.
if (id === "newsletter" && reply === "Approve") el.update(id, { status: "running", needsAction: null, progress: 0.15, eta: at(12) });
else if (id === "newsletter") el.update(id, { needsAction: "Which slot works better?", replies: ["Tue, Oct 7 · 9 AM", "Thu, Oct 9 · 6 PM"] });
else if (id === "retouch" && reply === "Approve") el.update(id, { status: "reviewed", needsAction: null });
else if (id === "retouch") el.update(id, { status: "running", needsAction: null, progress: 0.7, start: at(0), end: null, eta: at(20) });
else el.update(id, { status: "running", needsAction: null, progress: 0.1, start: at(0), eta: at(15) });
});
// Simulated live progress: running agents advance every few seconds.
const timer = setInterval(() => {
if (!el.isConnected) return clearInterval(timer);
for (const a of el.agents) {
if (a.status !== "running" || a.progress === null) continue;
const p = Math.min(1, a.progress + 0.006 + Math.random() * 0.01);
if (p < 1) { el.update(a.id, { progress: p }); continue; }
el.update(a.id, { status: "done", progress: 1, end: at(0) });
if (a.id === "copy-knit") el.update("copy-acc", { status: "running", start: at(0), progress: 0.02 });
}
}, 2500);
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
lens | board | inbox | tasks | calendar | graph | board | Active lens. Changing it animates every card to its new place (instant under reduced motion). |
label | string | Agents | Accessible name of the region (and of the lens tab list). |
Properties
| Name | 유형 | Description |
|---|---|---|
agents | Agent[] | The model: { id, name, goal?, status: "scheduled" | "running" | "blocked" | "done" | "reviewed", parent?, dependsOn?: id[], start?, eta?, end?, progress? (0-1), needsAction?: question, replies?: string[], trigger?: string, updated? }. Times are ms, Date or ISO strings. Cards are reused by id. Can be set before the element is defined, or given as a child <script type="application/json">. |
lens | string | Reflects the lens attribute. |
collapsed | string[] | Ids of collapsed parents in the Tasks lens. |
selected | string | null | Id of the selected agent (set without firing mv-agent-select). |
Methods
| Name | Description |
|---|---|
update(id, patch) | Merges a live change into one agent (an unknown id adds it). Progress-only changes refresh the card in place; status, dependencies, timing or questions relayout with FLIP. Batched to one layout per frame; status changes are announced. It is the element's own data method, not the base class's attribute hook (lens and label changes are handled internally), so it is safe to call from any framework; a subclass must not override it as a lifecycle hook. |
setLens(name) | Switches lens (same as setting the attribute). Returns false for an unknown lens. |
toggle(id, open?) | Collapses / expands a parent in the Tasks lens. |
removeItem(id) | Removes an agent; its card fades out. |
Events
| Name | Description |
|---|---|
mv-lens-change | The lens changed. detail: { lens, previous }. |
mv-agent-select | Click, Enter or Space on a card. detail: { id, agent }. |
mv-agent-move | Cancelable. A card was dropped in another board column (drag, Alt+arrows or the Move menu). detail: { id, from, to, meaning } (meaning is the plain-language intent, e.g. “unblock and resume the agent”). preventDefault() sends the card back. |
mv-agent-reply | A quick-reply button was pressed on an agent waiting for a human. detail: { id, reply, question }. The host decides what happens (usually update(id, { status, needsAction: null })). |
Content structure
| Name | Description |
|---|---|
<script type="application/json"> | Optional: initial agents as JSON, for server-rendered pages. |
CSS classes
| Name | Description |
|---|---|
mv-agent-lenses-card | The persistent card; data-density = card | item | row | bar | node, data-status, data-ask, data-selected. |
mv-agent-lenses-tab / -indicator | Lens switcher (role tablist) and its sliding surface. |
mv-agent-lenses-col / -intent | Board columns and the drop-intention label shown while dragging. |
mv-agent-lenses-edge / -edge-flow | Graph edges (data-kind dep | sub, data-state waiting | flowing | done | idle) and the flowing dash of running work. |
mv-agent-lenses-now | Calendar now-line. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-agent-lenses-max-height | none | Caps the panel height; content then scrolls inside (both axes). |
--mv-agent-lenses-bg | var(--mv-bg) | Background behind the lenses. |
--mv-agent-lenses-radius | var(--mv-radius-xl) | Outer corner radius. |
Accessibility
The lens switcher is a role="tablist" (roving focus, arrows switch lens) controlling a single role="tabpanel" that is re-labelled on every change; the change is announced with the agent count and pending actions. Cards are role="listitem" (board, inbox, calendar, graph) or role="treeitem" with aria-level, aria-setsize/posinset and aria-expanded (tasks). Each card's accessible name carries its name, status, progress, timing and relations (“waiting on …”, “subagent of …”); goal and question are its description. One tab stop (roving tabindex, the first card in reading order until a card has been focused): arrows move spatially in every lens, Home/End jump to the ends, Enter/Space selects; in Tasks, Right/Left expand, collapse or move between parent and children. On the board, Alt+Left/Right moves the card to the neighbouring column and Shift+F10 (or the Move button) opens a menu listing each column with what the move means; pointer drag shows that meaning on every column; touch uses the menu so scrolling stays free. Quick replies are real buttons reachable with Tab from the focused card; Escape returns to the card. Status is always an icon shape plus a text label, never color alone. A polite live region announces moves, replies, status changes and collapse summaries. Reduced motion (OS or data-motion="reduce"): instant relayout, no spinner, flowing dash or indeterminate bar. Decorative scaffolding (columns, axis, edges) is aria-hidden. Forced colors: card and time bar outlines, a Highlight selected lens, solid progress and now-line. Known limits: no drag and drop on touch (the Move menu replaces it); the graph uses a simple layered layout, so large graphs can show crossing edges; cards are not virtualized, so it is meant for a few dozen agents, not hundreds.