feat: add CLI Apps settings MVP
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ChatSummary,
|
||||
CliAppsPayload,
|
||||
ImageGenerationSettingsUpdate,
|
||||
ProviderSettingsUpdate,
|
||||
SettingsPayload,
|
||||
@@ -106,6 +107,24 @@ export async function fetchSettings(
|
||||
return request<SettingsPayload>(`${base}/api/settings`, token);
|
||||
}
|
||||
|
||||
export async function fetchCliApps(
|
||||
token: string,
|
||||
base: string = "",
|
||||
): Promise<CliAppsPayload> {
|
||||
return request<CliAppsPayload>(`${base}/api/settings/cli-apps`, token);
|
||||
}
|
||||
|
||||
export async function runCliAppAction(
|
||||
token: string,
|
||||
action: "install" | "update" | "uninstall" | "test",
|
||||
name: string,
|
||||
base: string = "",
|
||||
): Promise<CliAppsPayload> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("name", name);
|
||||
return request<CliAppsPayload>(`${base}/api/settings/cli-apps/${action}?${query}`, token);
|
||||
}
|
||||
|
||||
export async function listSlashCommands(
|
||||
token: string,
|
||||
base: string = "",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
ConnectionStatus,
|
||||
InboundEvent,
|
||||
Outbound,
|
||||
OutboundCliAppMention,
|
||||
OutboundImageGeneration,
|
||||
OutboundMedia,
|
||||
GoalStateWsPayload,
|
||||
@@ -304,7 +305,7 @@ export class NanobotClient {
|
||||
chatId: string,
|
||||
content: string,
|
||||
media?: OutboundMedia[],
|
||||
options?: { imageGeneration?: OutboundImageGeneration },
|
||||
options?: { imageGeneration?: OutboundImageGeneration; cliApps?: OutboundCliAppMention[] },
|
||||
): void {
|
||||
this.knownChats.add(chatId);
|
||||
const frame: Outbound = {
|
||||
@@ -313,6 +314,7 @@ export class NanobotClient {
|
||||
content,
|
||||
...(media && media.length > 0 ? { media } : {}),
|
||||
...(options?.imageGeneration ? { image_generation: options.imageGeneration } : {}),
|
||||
...(options?.cliApps?.length ? { cli_apps: options.cliApps } : {}),
|
||||
webui: true,
|
||||
};
|
||||
this.queueSend(frame);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ToolProgressEvent } from "@/lib/types";
|
||||
|
||||
/** Drop duplicate tool_call objects (same id or identical formatted trace). */
|
||||
export function dedupeToolCallsForUi(calls: unknown): unknown[] {
|
||||
if (!Array.isArray(calls) || calls.length === 0) return [];
|
||||
@@ -40,15 +42,60 @@ export function formatToolCallTrace(call: unknown): string | null {
|
||||
}
|
||||
|
||||
const VALID_PHASES = new Set(["start", "end", "error"]);
|
||||
const PHASE_RANK: Record<string, number> = { start: 1, end: 2, error: 3 };
|
||||
|
||||
export function toolTraceLinesFromEvents(events: unknown): string[] {
|
||||
export function normalizeToolProgressEvents(events: unknown): ToolProgressEvent[] {
|
||||
if (!Array.isArray(events)) return [];
|
||||
const seen = new Set<string>();
|
||||
const lines: string[] = [];
|
||||
const out: ToolProgressEvent[] = [];
|
||||
for (const event of events) {
|
||||
if (!event || typeof event !== "object") continue;
|
||||
const phase = (event as { phase?: unknown }).phase;
|
||||
const record = event as ToolProgressEvent;
|
||||
const phase = record.phase;
|
||||
if (!(phase && typeof phase === "string" && VALID_PHASES.has(phase))) continue;
|
||||
const name = typeof record.name === "string" ? record.name : "";
|
||||
const functionName =
|
||||
typeof (record as { function?: { name?: unknown } }).function?.name === "string"
|
||||
? String((record as { function?: { name?: unknown } }).function?.name)
|
||||
: "";
|
||||
if (!name && !functionName) continue;
|
||||
out.push(record);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function toolEventKey(event: ToolProgressEvent): string {
|
||||
if (event.call_id) return `call:${event.call_id}`;
|
||||
return formatToolCallTrace(event) ?? JSON.stringify(event);
|
||||
}
|
||||
|
||||
export function mergeToolProgressEvents(
|
||||
previous: ToolProgressEvent[] | undefined,
|
||||
incoming: ToolProgressEvent[],
|
||||
): ToolProgressEvent[] {
|
||||
if (!previous?.length) return incoming;
|
||||
if (!incoming.length) return previous;
|
||||
const next = [...previous];
|
||||
const indexByKey = new Map(next.map((event, index) => [toolEventKey(event), index]));
|
||||
for (const event of incoming) {
|
||||
const key = toolEventKey(event);
|
||||
const existingIndex = indexByKey.get(key);
|
||||
if (existingIndex === undefined) {
|
||||
indexByKey.set(key, next.length);
|
||||
next.push(event);
|
||||
continue;
|
||||
}
|
||||
const existing = next[existingIndex];
|
||||
const incomingRank = PHASE_RANK[String(event.phase)] ?? 0;
|
||||
const existingRank = PHASE_RANK[String(existing.phase)] ?? 0;
|
||||
next[existingIndex] = incomingRank >= existingRank ? { ...existing, ...event } : existing;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function toolTraceLinesFromEvents(events: unknown): string[] {
|
||||
const seen = new Set<string>();
|
||||
const lines: string[] = [];
|
||||
for (const event of normalizeToolProgressEvents(events)) {
|
||||
const callId = (event as { call_id?: unknown }).call_id;
|
||||
if (callId && typeof callId === "string") {
|
||||
if (seen.has(callId)) continue;
|
||||
|
||||
@@ -40,6 +40,9 @@ export interface UIMessage {
|
||||
/** For trace rows: each individual hint line, so consecutive hints can
|
||||
* render as a single collapsible group. */
|
||||
traces?: string[];
|
||||
/** Structured tool events behind trace rows. Kept so activity cards can
|
||||
* distinguish running, completed, and failed tool phases. */
|
||||
toolEvents?: ToolProgressEvent[];
|
||||
/** Activity rows: explicit file edits emitted by edit tools. */
|
||||
fileEdits?: UIFileEdit[];
|
||||
/** Activity rows created during the same agent phase share one collapsible block. */
|
||||
@@ -48,6 +51,8 @@ export interface UIMessage {
|
||||
images?: UIImage[];
|
||||
/** Signed or local UI-renderable media attachments. */
|
||||
media?: UIMediaAttachment[];
|
||||
/** App-specific CLI adapters explicitly attached to this user turn. */
|
||||
cliApps?: UICliAppAttachment[];
|
||||
/** Assistant turn: accumulated model reasoning / thinking text. Built up
|
||||
* incrementally from ``reasoning_delta`` frames; finalized when
|
||||
* ``reasoning_end`` arrives. */
|
||||
@@ -59,6 +64,15 @@ export interface UIMessage {
|
||||
latencyMs?: number;
|
||||
}
|
||||
|
||||
export interface UICliAppAttachment {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
category?: string;
|
||||
entry_point?: string;
|
||||
logo_url?: string | null;
|
||||
brand_color?: string | null;
|
||||
}
|
||||
|
||||
/** Structured UI blob on ``progress`` WS frames; channels may add more ``kind`` values later. */
|
||||
export interface AgentUIBlob {
|
||||
kind: string;
|
||||
@@ -252,6 +266,34 @@ export interface SettingsPayload {
|
||||
restart_required_sections?: Array<"runtime" | "web" | "image">;
|
||||
}
|
||||
|
||||
export interface CliAppInfo {
|
||||
name: string;
|
||||
display_name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
requires: string;
|
||||
source: string;
|
||||
entry_point: string;
|
||||
install_supported: boolean;
|
||||
installed: boolean;
|
||||
available: boolean;
|
||||
status: "installed" | "missing" | "available" | "unsupported" | "not_installed" | string;
|
||||
logo_url?: string | null;
|
||||
brand_color?: string | null;
|
||||
skill_installed: boolean;
|
||||
}
|
||||
|
||||
export interface CliAppsPayload {
|
||||
apps: CliAppInfo[];
|
||||
installed_count: number;
|
||||
catalog_updated_at?: string | null;
|
||||
last_action?: {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
output?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SettingsUpdate {
|
||||
model?: string;
|
||||
provider?: string;
|
||||
@@ -394,6 +436,15 @@ export interface OutboundImageGeneration {
|
||||
aspect_ratio?: string | null;
|
||||
}
|
||||
|
||||
export interface OutboundCliAppMention {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
category?: string;
|
||||
entry_point?: string;
|
||||
logo_url?: string | null;
|
||||
brand_color?: string | null;
|
||||
}
|
||||
|
||||
/** Response shape for ``GET .../webui-thread`` (server-built transcript replay). */
|
||||
export interface WebuiThreadPersistedPayload {
|
||||
schemaVersion: number;
|
||||
@@ -411,6 +462,7 @@ export type Outbound =
|
||||
content: string;
|
||||
media?: OutboundMedia[];
|
||||
image_generation?: OutboundImageGeneration;
|
||||
cli_apps?: OutboundCliAppMention[];
|
||||
/** Marks messages sent by the embedded WebUI, without changing the
|
||||
* generic websocket protocol for other clients. */
|
||||
webui?: true;
|
||||
|
||||
Reference in New Issue
Block a user