AG Studio Launch Week šŸš€šŸš€šŸš€ 28 Sep - 2 Oct 2026 šŸš€šŸš€šŸš€ Join now

Angular Embedded AnalyticsTool Components

Version 3.0.0

The chat panel renders a run of tool calls as a sequence of steps, one line each, with a marker carrying the outcome.

You declare two things per tool. label gives a step its words. detail gives it a component for the body that opens when the reader expands it: a result table, a preview, a confirmation prompt. A tool with no detail has no body, and its step does not expand.

This applies only when you use Studio's chat panel. An integration with no harness renders whatever it likes.

The example below opens on a conversation that has already run, so both kinds of detail are on screen straight away: Studio's own execute_query, and a web_search tool the example declares with a component of its own. Expand either, or send a message to watch the streaming and executing phases. The agent is scripted, so no LLM is involved.

Declare It Copy Link

Presentation is declared per tool name on the aiToolDisplay Studio property, not on the tool:

const studioProperties = {
    ai: ({ api }) => createAiHarness(api, () => ({ agents: [analyst], primary: 'analyst' })),
    aiToolDisplay: {
        list_reports: {
            label: ({ result }) => ({ text: result ? 'Looked up reports' : 'Looking up reports' }),
            detail: ReportListCard,
        },
        execute_query: { label: () => ({ text: 'Querying' }) },
        audit_log: { label: () => ({ text: '' }), hidden: true },
    },
};
Function
The words shown on the collapsed row, and optionally the value shown at its right-hand end. Called on every render, so it also owns the wording while the call is still in flight and the wording when it failed. Name what the call acted on rather than identifying it by id: the passed AgAiToolLabelParams.fieldName, AgAiToolLabelParams.widgetName and AgAiToolLabelParams.tableName resolve an id to the name the reader sees elsewhere.
detailCopy Link
any
A component rendering the expanded body of this tool's calls, receiving AgAiToolDetailParams. A tool that declares none has no expanded body, and its row is not interactive.
hiddenCopy Link
boolean
When true, calls to this tool are not shown in the message list at all.

Declarations are keyed by name, not by tool, because a conversation reloaded from history no longer has the tool that ran it - only the name it was called by. A replayed thread therefore renders the same as a live one, and these declarations apply to a harness you wrote yourself as well as to one built by createAiHarness.

Studio's own tools come pre-declared. An entry here replaces the declaration for that tool.

hidden: true leaves calls to that tool out of the message list entirely, which is useful for bookkeeping tools a user has no reason to see.

detail is typed for the framework you are writing in, so a component you pass is checked as one. That holds without a type annotation of your own, including in plain TypeScript, where detail is a component class.

Write the Label Copy Link

label is called on every render, so it owns the wording for every phase of the call, not only the finished one. It returns the step's words and, optionally, a short value shown at the right-hand end of the line.

Partial<TArgs>
The call's arguments so far - partial while they stream in.
statusCopy Link
AgAiToolDisplayStatus
Where the call has got to. done covers both outcomes - read result.success to tell them apart - awaiting_approval means it is waiting to be allowed to run, and cancelled means the run was stopped before the call settled.
resultCopy Link
AgAiToolResult
The call's outcome. Present once the call has settled.
fieldNameCopy Link
Function
The display name of a field, including any aggregation or date grain it carries.
widgetNameCopy Link
Function
A widget's title, or the name of its type when it has no title of its own.
tableNameCopy Link
Function
A table's display name.
label: ({ args, status, result, fieldName }) => {
    if (status === 'cancelled') {
        return { text: 'Looking up reports', pill: 'Stopped' };
    }
    if (result == null) {
        return { text: 'Looking up reports' };
    }
    if (!result.success) {
        return { text: 'Could not look up the reports', pill: `${result.issues.length} issues` };
    }
    return { text: 'Looked up reports', pill: `${result.data.reports.length} found` };
},

Describe the work rather than its outcome. The marker beside the step already says whether it finished, failed or was stopped, so "Added Revenue by region" reads better than "Successfully added widget".

Name what the call acted on rather than showing an id. fieldName, widgetName and tableName resolve an id to the name the reader sees elsewhere in the app, and return undefined when the id names nothing, such as a field that has since been deleted.

Streaming Arguments Copy Link

label runs on every render, so it sees the arguments as they arrive. Partially-parsed JSON means a field may be missing, and a string may be half-written, so guard every access:

// Wrong: throws mid-stream, before `team` has arrived.
label: ({ args }) => ({ text: `Looking up ${args.team.toUpperCase()}` }),

// Right: nothing to show yet is a normal state.
label: ({ args }) => ({ text: args.team ? `Looking up ${args.team}` : 'Looking up reports' }),

Write the Detail Copy Link

A detail component follows the same contract as any other Studio custom component: return an element, optionally refresh, optionally clean up. It renders only the body that opens beneath the step. The marker, the words and the pill stay Studio's, which is what keeps a long run readable however many tools contributed to it.

type Params = AgAiToolDetailParams<{ team?: string }, { reports: Report[] }>;

class ReportListCard {
    private element!: HTMLElement;

    init(params: Params) {
        this.element = document.createElement('div');
        this.render(params);
    }

    getGui() {
        return this.element;
    }

    refresh(params: Params) {
        this.render(params);
    }

    private render(params: Params) {
        const { result } = params;
        if (result == null) {
            this.element.textContent = `Looking up reports for ${params.args.team ?? 'all teams'}...`;
            return;
        }
        if (!result.success) {
            this.element.textContent = result.issues.map((issue) => issue.message).join(', ');
            return;
        }
        this.element.textContent = `${result.data?.reports.length ?? 0} reports`;
    }
}

The body brings no background or padding of its own. Content that needs to sit on its own surface should bring its own border.

In React, Angular or Vue, write a component in that framework. React and Angular components are passed directly. A Vue component is passed either directly or by the name it is registered under on the component hosting Studio, matching how a custom widget's comp is supplied. Switch the example above between frameworks to see each one.

What the Detail Receives Copy Link

The params are discriminated on status:

  • streaming - the model is still writing the arguments. args is partial: fields appear as they arrive, so guard every access.
  • ready - the arguments are complete and the call is queued.
  • executing - the tool is running.
  • done - result is authoritative, and is either a success carrying data or a failure carrying issues.
  • cancelled - the run was stopped before the call settled, so there may be no result at all.

data is whatever the tool's result or execute put there. Type both generics at your component's declaration to get it typed end to end.

In the panel, a step opens only once its call has settled, and a failed call opens onto its issues, listed by the panel rather than by your component. Your detail is therefore created with a successful result already in hand. The in-flight states above are what label sees on every render, and what a component sees if you drive one yourself.

Next Copy Link

  • Custom Tools - putting something in data worth rendering
  • Chat UI - what the panel does with a tool call