Node.js SDK

핸들러, 호출, 패널 UI 빌더, 컨텍스트 객체 — agentty-plugin.mjs의 전체 기능.

agentty-plugin.mjs는 의존성이 없는 파일 하나입니다. 타입 정의는 같은 위치의 agentty-plugin.d.ts에 있습니다. 플러그인 페이지에서 개발자 가이드를 누르면 둘 다 ~/.agentty/plugins/.sdk/에 풀립니다.

js
import { createPlugin, ui } from './agentty-plugin.mjs';

const plugin = createPlugin();
// 핸들러 등록…
plugin.start();

핸들러는 모두 start() 호출 전에 등록해야 합니다.

핸들러#

모든 핸들러는 async일 수 있습니다. 오류는 로그에 남고 이용자에게 알림으로 표시됩니다.

핸들러호출 시점
onActivate(info => …)플러그인이 시작됨. infoplugin.dataDir, language, context
command(id, ({ context, args }) => …)팔레트나 페인 바 버튼에서 명령 실행
onPanelOpen(context => …)패널이 보이게 됨 — 여기서 그립니다
onPanelClose(context => …)패널이 숨겨짐
onEvent(elementId, (event, context) => …)해당 id의 UI 요소가 사용됨
onAnyEvent((event, context) => …)onEvent가 처리하지 않은 모든 UI 이벤트
onContextChange(context => …)포커스된 페인, 그 상태, 폴더가 바뀜
onUrl(path, ({ path, query, url }) => …)agentty://plugin/<id>/<path>?…가 열림
onShutdown(() => …)Agentty가 플러그인을 중단시킴

호출#

모든 호출은 프라미스를 반환합니다.

호출권한
setPanel(tree) — 패널 내용 교체
showPanel() — 이 플러그인의 패널 열기
notify(message, kind)info, success, warning, error
setBadge(text) — 탭 영역 버튼에 최대 8자
getContext()
openUrl(url) — http/https
revealPath(path) — 파일 관리자에서 보기workspace.read
injectPrompt(request)prompt.inject
sendToTerminal({ paneId, text, submit })terminal.write
getSession({ paneId, maxTurns })session.read
listWorkspaces()workspace.read
log(...) — 플러그인 로그(stderr)에 기록

plugin.info에는 initialize 데이터가, plugin.context에는 최신 컨텍스트가 들어 있습니다.

패널#

패널은 폭 360px이고 세로로 스크롤됩니다. 플러그인이 트리로 설명하면 Agentty가 네이티브로 그리므로 앱과 이질감이 없고 웹뷰가 필요 없습니다. 무언가 바뀔 때마다 새 트리를 보내면 됩니다. 텍스트 필드는 다른 value를 보내지 않는 한 이용자가 입력한 값을 유지합니다.

빌더요소이벤트
ui.column(children, { gap }) / ui.row(children, { gap, wrap })레이아웃. gap: none, small, medium, large
ui.section(title, children)제목이 있는 묶음
ui.text(text, style)body, title, muted, small, code, error, success
ui.button(id, label, { icon, variant, disabled })primary, secondary, ghost, dangerclick
ui.input(id, { placeholder, value })한 줄 입력입력이 멈추면 change, Enter에 submit. event.value가 텍스트
ui.list(id, items, { empty }){ id, title, subtitle, detail, icon, tone, actions }event.item과 함께 select, 행 버튼은 event.item·event.action과 함께 action
ui.choice(id, [{ value, label }], value)분할 선택값과 함께 change
ui.toggle(id, label, value)스위치새 불리언과 함께 change
ui.badge(text, tone)neutral, info, success, warning, error
ui.spinner(text)
ui.divider()

null과 false인 자식은 건너뛰므로 조건 && ui.text('…')가 그대로 동작합니다.

js
plugin.onPanelOpen(async (context) => {
  const notes = await search('');
  plugin.setPanel(
    ui.column([
      ui.input('q', { placeholder: 'Search notes' }),
      ui.list('notes', notes.map((n) => ({
        id: n.path,
        title: n.title,
        subtitle: n.folder,
        icon: 'notebook',
        actions: [{ id: 'insert', icon: 'send', tooltip: 'Insert into the focused pane' }],
      })), { empty: 'No notes yet' }),
    ]),
  );
});

plugin.onEvent('notes', (event, context) => {
  if (event.event === 'action' && event.action === 'insert') {
    return plugin.injectPrompt({ text: read(event.item), target: 'ask' });
  }
});
Note

제한: 요소 2,000개, 깊이 12단계, 텍스트당 20,000자. 패널은 최소 50ms 간격으로 다시 그려지고 알림은 최소 700ms 간격입니다. 초당 240건을 넘게 보내는 플러그인은 폭주로 간주되어 중단됩니다.

컨텍스트#

모든 명령·이벤트·패널 호출에는 포커스된 창의 컨텍스트가 실립니다.

json
{
  "workspace": { "id": 3, "name": "agentty", "cwd": "/Users/me/agentty", "active": true },
  "pane": {
    "id": 12,
    "kind": "claude",
    "tool": "claude",
    "title": "Claude Code",
    "cwd": "/Users/me/agentty",
    "sessionId": "…",
    "status": "idle",
    "running": true
  },
  "language": "ko"
}

플러그인이 보는 범위는 선언한 권한에 따라 달라집니다. 폴더와 이름 필드(workspace.cwd, workspace.name, pane.cwd, pane.title)에는 workspace.read가, pane.sessionId에는 session.read가 필요합니다. 권한이 없으면 id, kind, tool, status, running, 언어만 남습니다. 어느 페인이 포커스됐는지는 알 수 있어도 이용자가 어디서 작업하는지는 알 수 없습니다.

kindclaude, codex, shell입니다. 다른 에이전트 CLI는 shell 페인에서 실행되며 tool이 이름을 알려줍니다.

status의미
idle입력 대기
working도구 실행 중
thinking턴이 열린 상태, 도구 호출 사이
finished턴 종료
permission권한을 묻는 중
question이용자에게 질문 중
interrupted이용자가 중단함
shell일반 셸
exited프로그램 종료

방해하지 않을 판단을 할 때 workingthinking은 같게 취급하세요.

프롬프트 보내기#

js
await plugin.injectPrompt({
  text: 'Continue the release checklist.',
  title: 'Release',          // 새 세션의 워크스페이스 이름이자 다이얼로그 제목
  target: 'ask',             // ask | active | newWorkspace | newTab | pane | workspace
  agent: 'claude',           // 새 세션용 claude | codex | shell
  cwd: '/Users/me/project',  // 새 세션의 폴더
  submit: true,              // Enter 누르기 (에이전트만)
});
  • ask(기본값)는 **보낼 곳…**을 띄워 이용자가 목적지를 고르게 합니다.
  • active는 포커스된 페인에, panepaneId에, workspaceworkspaceId에 입력하고, newWorkspace·newTab은 그 프롬프트로 새 세션을 시작합니다.
  • 터미널에는 언제나 입력만 됩니다. injectPrompt는 Enter를 누르지 않습니다.
  • 60,000바이트가 넘는 프롬프트는 ~/.agentty/prompts/에 저장되고 에이전트에게 그 파일을 읽으라고 전달됩니다.

다른 앱이나 링크에서 시작된 것이라면 ask를 쓰세요.

세션과 터미널#

js
const session = await plugin.getSession({ paneId: context.pane.id, maxTurns: 200 });
// { agent, sessionId, title, cwd, status, turnCount, turns: [{ role: 'user' | 'assistant', text }] }

await plugin.sendToTerminal({
  paneId: context.pane.id,
  text: 'Summarize what we did.',
  submit: true,
});

에이전트에 입력하기 전에 pane.status를 확인하세요. working, permission, question은 방해하면 안 됩니다.

다음으로#