Skip to content

Commit 390d9b5

Browse files
apievangelistclaude
andcommitted
Per-operation 'Complete this operation' + default back to live apis.io
Prefer working the live surface operation by operation. Default example is apis.io (live) again; the bulk target roadmap is demoted to a reference example. Each operation's ✨ now leads with 'Complete this operation' — one precise Claude pass returning the tool + prompts + resources + skill for THAT operation together (scoping to one op makes each part precise and consistent), rendered as four add-able sections. Per-kind suggestions remain below it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ebf8d0f commit 390d9b5

3 files changed

Lines changed: 128 additions & 5 deletions

File tree

src/main.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ import { buildExperience, deriveSurface, computeCoverage, type ExperienceModel,
1515
import { renderExperience } from './render';
1616
import { initEngage } from './engage';
1717
import { initSettings, openSettings, getToken } from './settings';
18-
import { suggest, applySuggestion, describe, type SuggestKind } from './suggest';
18+
import { suggest, applySuggestion, describe, completeOperation, type SuggestKind, type Suggestion } from './suggest';
1919
import { downloadBundle } from './bundle';
2020
import { esc, escAttr } from './ui';
2121

2222
// `src` is what we actually fetch (must be same-origin or CORS-enabled); `show` is the
2323
// human label. apis.io/apis.json has no CORS header, so we bundle it locally and let the
2424
// tool fetch its OpenAPI from githubusercontent (which does allow cross-origin).
2525
const DEFAULTS = [
26-
{ label: 'APIs.io — target surface', src: './examples/apis-io-target.json', show: 'apis.io roadmap', blurb: 'The proposed robust API → MCP → Agent-Skill buildout for apis.io — 54 operations, 11 prompts, 12 resources, 10 skills. A roadmap, not the live contract.' },
27-
{ label: 'APIs.io — live', src: './examples/apis-io.json', show: 'apis.io/apis.json', blurb: 'The API → MCP → Agent-Skill surface deployed on apis.io today: discovery free, synthesis Pro.' },
26+
{ label: 'APIs.io', src: './examples/apis-io.json', show: 'apis.io/apis.json', blurb: 'The live API → MCP → Agent-Skill surface of apis.io. Work it operation by operation — the ✨ on each row completes that one operation precisely.' },
2827
{ label: 'API Evangelist', src: 'https://apievangelist.com/apis.yml', show: 'apievangelist.com/apis.yml', blurb: 'The API Evangelist network index.' },
28+
{ label: 'APIs.io — target (roadmap)', src: './examples/apis-io-target.json', show: 'apis.io roadmap', blurb: 'A bulk proposed buildout, for reference — prefer completing operations one at a time on the live surface above.' },
2929
];
3030

3131
let current: ApisDoc | null = null;
@@ -195,7 +195,9 @@ function openOpMenu(anchor: HTMLElement, op: ExpOperation): void {
195195
const el = document.createElement('div');
196196
el.className = 'op-menu';
197197
el.innerHTML = `
198-
<div class="op-menu-h">Suggest for ${esc(op.method)} ${esc(op.path)}</div>
198+
<div class="op-menu-h">Complete ${esc(op.method)} ${esc(op.path)}</div>
199+
<button data-complete="1"><strong>✨ Complete this operation</strong><span class="op-menu-sub">tool + prompts + resources + skill, in one pass</span></button>
200+
<div class="op-menu-div"></div>
199201
<button data-k="tool">⚙ MCP tool</button>
200202
<button data-k="prompt">◇ MCP prompt</button>
201203
<button data-k="resource">▤ MCP resource</button>
@@ -204,12 +206,82 @@ function openOpMenu(anchor: HTMLElement, op: ExpOperation): void {
204206
document.body.appendChild(el);
205207
const r = anchor.getBoundingClientRect();
206208
el.style.top = `${window.scrollY + r.bottom + 4}px`;
207-
el.style.left = `${window.scrollX + Math.min(r.left, window.innerWidth - 220)}px`;
209+
el.style.left = `${window.scrollX + Math.min(r.left, window.innerWidth - 240)}px`;
210+
el.querySelector<HTMLButtonElement>('button[data-complete]')!.addEventListener('click', () => { closeOpMenu(); runComplete(op); });
208211
el.querySelectorAll<HTMLButtonElement>('button[data-k]').forEach((b) =>
209212
b.addEventListener('click', () => { closeOpMenu(); runSuggest(b.dataset.k as SuggestKind, op); }));
210213
opMenuEl = el;
211214
}
212215

216+
async function runComplete(op: ExpOperation): Promise<void> {
217+
const token = getToken();
218+
if (!token) { openSettings(); return; }
219+
const exp = targetApi();
220+
if (!exp) { alert('Load an API that has an OpenAPI first.'); return; }
221+
completeModal(op, exp, 'loading', null);
222+
try {
223+
const res = await completeOperation(exp, op, token);
224+
completeModal(op, exp, 'ready', res);
225+
} catch (e) {
226+
completeModal(op, exp, 'error', null, e instanceof Error ? e.message : String(e));
227+
}
228+
}
229+
230+
let cModal: HTMLElement | null = null;
231+
function completeModal(op: ExpOperation, exp: ExpApi, state: string, res: Awaited<ReturnType<typeof completeOperation>> | null, err = ''): void {
232+
if (!cModal) {
233+
cModal = document.createElement('div');
234+
cModal.className = 'modal suggest-modal';
235+
document.body.appendChild(cModal);
236+
cModal.addEventListener('click', (e) => { if (e.target === cModal) cModal!.hidden = true; });
237+
}
238+
const section = (title: string, kind: SuggestKind, items: Suggestion[]) => {
239+
if (!items.length) return `<div class="cx-sec"><div class="cx-h">${esc(title)}</div><p class="muted cx-empty">Nothing suggested — this part looks covered.</p></div>`;
240+
const hasTier = kind !== 'skill';
241+
return `<div class="cx-sec"><div class="cx-h">${esc(title)}</div>${items.map((s, i) => {
242+
const def = s.tier || (kind === 'tool' && op.tier === 'pro' ? 'pro' : 'free');
243+
return `<div class="sug-item" data-kind="${kind}" data-i="${i}">
244+
<div class="sug-text"><code>${esc(describe(kind, s))}</code>${s.description ? `<span class="sug-desc">${esc(s.description)}</span>` : ''}</div>
245+
<div class="sug-actions">
246+
${hasTier ? `<span class="sug-tier"><button type="button" class="tglt ${def === 'free' ? 'on' : ''}" data-t="free">Free</button><button type="button" class="tglt ${def === 'pro' ? 'on' : ''}" data-t="pro">Pro</button></span>` : ''}
247+
<button class="btn sug-add">+ Add</button>
248+
</div>
249+
</div>`; }).join('')}</div>`;
250+
};
251+
const body = state === 'loading'
252+
? `<div class="loading"><div class="spinner"></div><p>Completing <code>${esc(op.method)} ${esc(op.path)}</code>…</p></div>`
253+
: state === 'error'
254+
? `<div class="error-box"><p>${esc(err)}</p></div>`
255+
: `<p class="sug-intro">Precise proposal for <code>${esc(op.method)} ${esc(op.path)}</code> — add the parts you want; each edits the in-memory OpenAPI and re-renders.</p>
256+
${section('⚙ MCP tool', 'tool', res!.tools)}
257+
${section('◇ MCP prompts', 'prompt', res!.prompts)}
258+
${section('▤ MCP resources', 'resource', res!.resources)}
259+
${section('✦ Agent Skill', 'skill', res!.skills)}`;
260+
cModal.innerHTML = `<div class="modal-card suggest-card">
261+
<div class="modal-head"><span>✨ Complete ${esc(op.method)} ${esc(op.path)}</span><button type="button" class="sug-close" aria-label="Close">×</button></div>
262+
<div class="suggest-body">${body}</div>
263+
</div>`;
264+
cModal.hidden = false;
265+
cModal.querySelector('.sug-close')!.addEventListener('click', () => { cModal!.hidden = true; });
266+
cModal.querySelectorAll<HTMLButtonElement>('.tglt').forEach((t) => t.addEventListener('click', () => {
267+
const wrap = t.closest('.sug-tier')!; wrap.querySelectorAll('.tglt').forEach((x) => x.classList.remove('on')); t.classList.add('on');
268+
}));
269+
if (res) cModal.querySelectorAll<HTMLButtonElement>('.sug-add').forEach((btn) => btn.addEventListener('click', () => {
270+
const item = btn.closest<HTMLElement>('.sug-item')!;
271+
const kind = item.dataset.kind as SuggestKind;
272+
const list = kind === 'tool' ? res.tools : kind === 'prompt' ? res.prompts : kind === 'resource' ? res.resources : res.skills;
273+
const s = { ...list[Number(item.dataset.i)] };
274+
const onTier = item.querySelector<HTMLElement>('.tglt.on');
275+
if (onTier) s.tier = onTier.dataset.t as 'free' | 'pro';
276+
applySuggestion(kind, exp, s, op);
277+
deriveSurface(exp);
278+
if (model) model.coverage = computeCoverage(model.apis);
279+
editCount++;
280+
reRender();
281+
btn.textContent = '✓ Added'; btn.disabled = true;
282+
}));
283+
}
284+
213285
async function runSuggest(kind: SuggestKind, op?: ExpOperation): Promise<void> {
214286
const token = getToken();
215287
if (!token) { openSettings(); return; }

src/style.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,13 @@ div.resp { display: flex; align-items: center; gap: 0.6rem; padding: 0.4rem 0.6r
484484
.sug-tier .tglt + .tglt { border-left: 1px solid var(--line); }
485485
.sug-tier .tglt.on[data-t=free] { background: color-mix(in srgb, var(--ok) 15%, transparent); color: var(--ok); }
486486
.sug-tier .tglt.on[data-t=pro] { background: var(--accent-soft); color: var(--accent); }
487+
488+
/* Complete-operation menu item + modal sections */
489+
.op-menu button[data-complete] { display: flex; flex-direction: column; align-items: flex-start; gap: .1rem; }
490+
.op-menu button[data-complete] strong { color: var(--accent); }
491+
.op-menu-sub { font-size: .68rem; color: var(--muted); }
492+
.op-menu-div { height: 1px; background: var(--line-soft); margin: .1rem 0; }
493+
.cx-sec { margin-bottom: 1rem; }
494+
.cx-h { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .3px; color: var(--brand); margin: 0 0 .4rem; padding-bottom: .25rem; border-bottom: 1px solid var(--line-soft); }
495+
.cx-empty { font-size: .82rem; margin: .2rem 0; }
496+
.cx-sec .sug-item { margin-bottom: .4rem; }

src/suggest.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,47 @@ Return ONLY the suggestions object.`;
8787
return (out.suggestions || []).slice(0, 8);
8888
}
8989

90+
export interface CompleteResult { tools: Suggestion[]; prompts: Suggestion[]; resources: Suggestion[]; skills: Suggestion[]; }
91+
92+
// Precise, operation-scoped pass: ask ONE question about a single operation and get a coherent
93+
// set across all four dimensions at once — the right tool, the prompts that would orchestrate it,
94+
// the resources it should back, and the skill it belongs to. Scoping to one op makes each part
95+
// precise (and mutually consistent) in a way per-kind suggestions can't be.
96+
export async function completeOperation(exp: ExpApi, op: ExpOperation, token: string): Promise<CompleteResult> {
97+
const it = (props: Record<string, unknown>, required: string[]) =>
98+
({ type: 'object', additionalProperties: false, properties: props, required });
99+
const schema = {
100+
type: 'object', additionalProperties: false, required: ['tools', 'prompts', 'resources', 'skills'],
101+
properties: {
102+
tools: { type: 'array', items: it({ mcpTool: strT, description: strT, tier: tierEnum }, ['mcpTool', 'description']) },
103+
prompts: { type: 'array', items: it({ name: strT, tier: tierEnum, description: strT }, ['name', 'tier', 'description']) },
104+
resources: { type: 'array', items: it({ uri: strT, tier: tierEnum, description: strT }, ['uri', 'tier', 'description']) },
105+
skills: { type: 'array', items: it({ name: strT, description: strT }, ['name', 'description']) },
106+
},
107+
};
108+
const prompt = `Complete the DX/AX for exactly ONE operation. Propose precisely, and only what genuinely improves THIS operation — empty arrays are fine:
109+
- tools: the MCP tool this operation should expose if it lacks a good one (0-1; leave empty if "tool" below is already right)
110+
- prompts: MCP prompts that would orchestrate this operation's tool (0-3)
111+
- resources: resources this operation should back / expose (0-2)
112+
- skills: the single Agent Skill this operation belongs to (0-1; a short kebab-case slug)
113+
114+
Focus operation:
115+
${op.method} ${op.path} (id:${op.operationId || '?'}, tier:${op.tier}, tool:${op.mcpTool || '— none —'}, skill:${op.agentSkill || '— none —'})
116+
117+
Full API surface for context (don't duplicate what exists; reuse existing tool/prompt/resource names where they'd apply):
118+
119+
${context(exp)}
120+
121+
Return ONLY the object.`;
122+
const out = await callClaudeJSON<CompleteResult>({ token, system: getGuideSkill(), prompt, schema, maxTokens: 3000 });
123+
return {
124+
tools: (out.tools || []).slice(0, 2),
125+
prompts: (out.prompts || []).slice(0, 4),
126+
resources: (out.resources || []).slice(0, 3),
127+
skills: (out.skills || []).slice(0, 2),
128+
};
129+
}
130+
90131
// A one-line human label for a suggestion in the picker.
91132
export function describe(kind: SuggestKind, s: Suggestion): string {
92133
if (kind === 'path') return `${s.method} ${s.path}${s.mcpTool} [${s.tier}]`;

0 commit comments

Comments
 (0)