Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Core data is local-first:
- Config: `~/.agenty/config.json`
- Sessions: append-only JSONL under `~/.agenty/sessions/`
- Session projection: `~/.agenty/agenty.sqlite`
- Providers/models: `~/.agenty/providers/`
- Providers/models: `~/.agenty/providers/<provider-code>.json` (models embedded)
- Agents: `~/.agenty/agents/`
- Logs: `~/.agenty/logs/<yyyy>/<mm>/<dd>/core.log`

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Core stores data under `~/.agenty` by default. Pass `--data-dir <path>` to the C
| Configuration | `~/.agenty/config.json` |
| Session transcripts | `~/.agenty/sessions/<yyyy>/<mm>/<dd>/<session-id>.jsonl` |
| Session index | `~/.agenty/agenty.sqlite` |
| Providers and models | `~/.agenty/providers/` |
| Providers and models | `~/.agenty/providers/<provider-code>.json` (models embedded) |
| Agents | `~/.agenty/agents/` |
| Logs | `~/.agenty/logs/<yyyy>/<mm>/<dd>/core.log` |

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ core 默认把数据保存在 `~/.agenty`。可向 CLI 传入 `--data-dir <path>
| 配置 | `~/.agenty/config.json` |
| 会话 transcript | `~/.agenty/sessions/<yyyy>/<mm>/<dd>/<session-id>.jsonl` |
| 会话索引 | `~/.agenty/agenty.sqlite` |
| Providers 和 models | `~/.agenty/providers/` |
| Providers 和 models | `~/.agenty/providers/<provider-code>.json`(模型内嵌) |
| Agents | `~/.agenty/agents/` |
| 日志 | `~/.agenty/logs/<yyyy>/<mm>/<dd>/core.log` |

Expand Down
44 changes: 6 additions & 38 deletions packages/agenty-cli/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useRenderer, useSelectionHandler } from "@opentui/react";
import { useState } from "react";

import type { ChatSessionDto, ModelDto } from "./api/types";
import type { ChatSessionDto } from "./api/types";
import { commands, parseCommandTokens } from "./commands/registry";
import { AgentOverlay } from "./components/AgentOverlay";
import { BottomDialog, useBottomDialogSize } from "./components/BottomDialog";
import { BottomDialog } from "./components/BottomDialog";
import { CommandPalette } from "./components/CommandPalette";
import { InputBox } from "./components/InputBox";
import { LogoHeader } from "./components/LogoHeader";
import { MessageList } from "./components/MessageList";
import { ModelOverlay } from "./components/ModelOverlay";
import { ProviderOverlay } from "./components/ProviderOverlay";
import { SelectOverlay } from "./components/SelectOverlay";
import { StatusOverlay } from "./components/StatusOverlay";
Expand All @@ -24,7 +25,7 @@ const INPUT_TOP_GAP = 1;
const PROVIDER_OVERLAY_HEIGHT = 18;
const AGENTS_OVERLAY_HEIGHT = 18;
const STATUS_OVERLAY_HEIGHT = 14;
const MODEL_OVERLAY_HEIGHT = 18;
const MODEL_OVERLAY_HEIGHT = 20;

function panelHeight(overlay: OverlayKind): number | null {
switch (overlay) {
Expand Down Expand Up @@ -275,7 +276,7 @@ function ChatView() {
onTab={handleTab}
streaming={busy}
phrase={chat.phrase}
modelName={`${app.model?.providerName ?? "?"}/${app.model?.name ?? "?"}`}
modelName={`${app.model?.providerName ?? "?"} · ${app.model?.name ?? "?"}`}
cwd={app.session?.cwd ?? process.cwd()}
contextWindow={app.session?.contextWindow ?? 0}
tokenConsumed={chat.tokenConsumed}
Expand Down Expand Up @@ -303,12 +304,8 @@ function OverlayPanel({
}: {
kind: "provider" | "agents" | "status" | "model-select";
}) {
const app = useApp();
return kind === "model-select" ? (
<ModelSelectOverlay
onClose={() => app.setOverlay(null)}
onSelect={(model) => void app.switchModel(model)}
/>
<ModelOverlay />
) : kind === "provider" ? (
<ProviderOverlay />
) : kind === "agents" ? (
Expand All @@ -318,35 +315,6 @@ function OverlayPanel({
) : <StatusOverlay />;
}

function ModelSelectOverlay({
onClose,
onSelect,
}: {
onClose: () => void;
onSelect: (model: ModelDto) => void;
}) {
const client = useAppStore((s) => s.client);
const dialogSize = useBottomDialogSize();
return (
<SelectOverlay<ModelDto>
title="Switch Model"
dialog
visibleOptionCount={Math.max(dialogSize.height - 2, 1)}
emptyHint="No switchable chat models found"
onClose={onClose}
onSelect={onSelect}
load={async () => {
const models = client ? await client.listModels() : [];
return models
.map((m) => ({
label: `${m.providerName}/${m.name}`,
data: m,
}));
}}
/>
);
}

function SessionSelectOverlay({
onClose,
onSelect,
Expand Down
24 changes: 12 additions & 12 deletions packages/agenty-cli/src/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ describe("AgentyClient session list", () => {

await expect(client.isInitialized()).resolves.toBe(false);
await expect(client.completeInitialization({
agentSlug: "default",
providerSlug: "openai",
modelSlug: "gpt-test",
agentCode: "default",
providerCode: "openai",
modelCode: "gpt-test",
})).resolves.toEqual({ initialized: false });
});

Expand All @@ -40,7 +40,7 @@ describe("AgentyClient session list", () => {

test("normalizes null provider models before model projection", async () => {
const provider = {
slug: "empty",
code: "empty",
name: "Empty",
type: "openai",
baseUrl: "https://example.invalid",
Expand Down Expand Up @@ -93,7 +93,7 @@ describe("AgentyClient session list", () => {
test("normalizes null session collections for old core responses", async () => {
const session = {
id: "session",
agentSlug: "default",
agentCode: "default",
contextWindow: 128000,
rounds: null,
createdAt: "2026-01-01T00:00:00Z",
Expand All @@ -108,16 +108,16 @@ describe("AgentyClient session list", () => {
});

test("updates a resumed session when an explicit model is requested", async () => {
const agent = { slug: "default", name: "Default" } as AgentDto;
const currentModel = { providerSlug: "openai", modelSlug: "gpt-old" };
const agent = { code: "default", name: "Default" } as AgentDto;
const currentModel = { providerCode: "openai", modelCode: "gpt-old" };
const requestedModel = {
slug: "gpt-new",
providerSlug: "openai",
code: "gpt-new",
providerCode: "openai",
providerName: "OpenAI",
} as ModelDto;
const existing = {
id: "session",
agentSlug: "default",
agentCode: "default",
currentModel,
rounds: [],
} as unknown as ChatSessionDto;
Expand All @@ -128,7 +128,7 @@ describe("AgentyClient session list", () => {
client.getLastSessionByAgent = async () => existing;
client.setSessionModel = async (_id, model) => {
updatedWith = model;
return { ...existing, currentModel: { providerSlug: model.providerSlug, modelSlug: model.slug } };
return { ...existing, currentModel: { providerCode: model.providerCode, modelCode: model.code } };
};

const prepared = await client.prepareSession({
Expand All @@ -139,6 +139,6 @@ describe("AgentyClient session list", () => {

expect(updatedWith).toBe(requestedModel);
expect(prepared.model).toBe(requestedModel);
expect(prepared.session.currentModel).toEqual({ providerSlug: "openai", modelSlug: "gpt-new" });
expect(prepared.session.currentModel).toEqual({ providerCode: "openai", modelCode: "gpt-new" });
});
});
84 changes: 42 additions & 42 deletions packages/agenty-cli/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class AgentyClient {
return agents.find((agent) => agent.isDefault) ?? agents[0];
}
const lower = reference.toLowerCase();
const matched = agents.find((agent) => agent.slug === reference) ??
const matched = agents.find((agent) => agent.code === reference) ??
agents.find((agent) => agent.name.toLowerCase() === lower);
if (!matched) {
throw new Error(`agent not found: ${reference}`);
Expand All @@ -96,16 +96,16 @@ export class AgentyClient {
return agent;
}

async updateAgent(slug: string, input: UpdateAgentDto): Promise<AgentDto> {
const agent = await this.rpc.call<AgentDto | null>("agent.update", { slug, ...input });
async updateAgent(code: string, input: UpdateAgentDto): Promise<AgentDto> {
const agent = await this.rpc.call<AgentDto | null>("agent.update", { code, ...input });
if (!agent) {
throw new Error(`core returned an empty agent for ${slug}`);
throw new Error(`core returned an empty agent for ${code}`);
}
return agent;
}

async deleteAgent(slug: string): Promise<void> {
await this.rpc.call("agent.delete", { slug });
async deleteAgent(code: string): Promise<void> {
await this.rpc.call("agent.delete", { code });
}

async listProviders(): Promise<ModelProviderDto[]> {
Expand All @@ -127,16 +127,16 @@ export class AgentyClient {
return normalizeProvider(provider);
}

async updateProvider(slug: string, input: UpdateModelProviderDto): Promise<ModelProviderDto> {
const provider = await this.rpc.call<ModelProviderDto | null>("provider.update", { slug, ...input });
async updateProvider(code: string, input: UpdateModelProviderDto): Promise<ModelProviderDto> {
const provider = await this.rpc.call<ModelProviderDto | null>("provider.update", { code, ...input });
if (!provider) {
throw new Error(`core returned an empty provider for ${slug}`);
throw new Error(`core returned an empty provider for ${code}`);
}
return normalizeProvider(provider);
}

async deleteProvider(slug: string): Promise<void> {
await this.rpc.call("provider.delete", { slug });
async deleteProvider(code: string): Promise<void> {
await this.rpc.call("provider.delete", { code });
}

async listModels(): Promise<ModelDto[]> {
Expand Down Expand Up @@ -164,9 +164,9 @@ export class AgentyClient {
const models = await this.listModels();
const lower = reference.toLowerCase();
const matches = models.filter((model) =>
model.slug === reference ||
model.code === reference ||
model.name.toLowerCase() === lower ||
`${model.providerSlug}/${model.slug}`.toLowerCase() === lower ||
`${model.providerCode}/${model.code}`.toLowerCase() === lower ||
`${model.providerName}/${model.name}`.toLowerCase() === lower,
);
if (matches.length !== 1) {
Expand All @@ -177,27 +177,27 @@ export class AgentyClient {

async createModel(input: CreateModelDto): Promise<ModelDto> {
const provider = await this.rpc.call<ModelProviderDto | null>("provider.addModel", input);
return findProjectedModel(provider, input.modelSlug);
return findProjectedModel(provider, input.modelCode);
}

async updateModel(providerSlug: string, modelSlug: string, input: UpdateModelDto): Promise<ModelDto> {
async updateModel(providerCode: string, modelCode: string, input: UpdateModelDto): Promise<ModelDto> {
const provider = await this.rpc.call<ModelProviderDto | null>("provider.addModel", {
providerSlug,
modelSlug,
providerCode,
modelCode,
...input,
});
return findProjectedModel(provider, modelSlug);
return findProjectedModel(provider, modelCode);
}

async deleteModel(providerSlug: string, modelSlug: string): Promise<void> {
await this.rpc.call("provider.removeModel", { providerSlug, modelSlug });
async deleteModel(providerCode: string, modelCode: string): Promise<void> {
await this.rpc.call("provider.removeModel", { providerCode, modelCode });
}

async createSession(agentSlug: string, model: ModelDto, effort: ReasoningEffort = "off"): Promise<ChatSessionDto> {
async createSession(agentCode: string, model: ModelDto, effort: ReasoningEffort = "off"): Promise<ChatSessionDto> {
const session = await this.rpc.call<ChatSessionDto | null>("session.create", {
agentSlug,
providerSlug: model.providerSlug,
modelSlug: model.slug,
agentCode,
providerCode: model.providerCode,
modelCode: model.code,
contextWindow: model.contextWindow,
reasoningEffort: effort,
});
Expand All @@ -209,21 +209,21 @@ export class AgentyClient {
return requireSession(session, `session.get ${id}`);
}

async listSessionSummaries(agentSlug?: string): Promise<SessionSummaryDto[]> {
async listSessionSummaries(agentCode?: string): Promise<SessionSummaryDto[]> {
const summaries = await this.rpc.call<Array<SessionSummaryDto | null> | null>(
"session.list",
agentSlug ? { agentSlug } : {},
agentCode ? { agentCode } : {},
);
return (summaries ?? []).filter((summary): summary is SessionSummaryDto => summary !== null);
}

async listSessions(agentSlug?: string): Promise<ChatSessionDto[]> {
const summaries = await this.listSessionSummaries(agentSlug);
async listSessions(agentCode?: string): Promise<ChatSessionDto[]> {
const summaries = await this.listSessionSummaries(agentCode);
return Promise.all(summaries.map((session) => this.getSession(session.id)));
}

async getLastSessionByAgent(agentSlug: string): Promise<ChatSessionDto | null> {
const sessions = await this.listSessionSummaries(agentSlug);
async getLastSessionByAgent(agentCode: string): Promise<ChatSessionDto | null> {
const sessions = await this.listSessionSummaries(agentCode);
return sessions.length > 0 ? this.getSession(sessions[0].id) : null;
}

Expand All @@ -235,8 +235,8 @@ export class AgentyClient {
async setSessionModel(id: string, model: ModelDto): Promise<ChatSessionDto> {
const session = await this.rpc.call<ChatSessionDto | null>("session.setModel", {
id,
providerSlug: model.providerSlug,
modelSlug: model.slug,
providerCode: model.providerCode,
modelCode: model.code,
});
return requireSession(session, `session.setModel ${id}`);
}
Expand Down Expand Up @@ -271,20 +271,20 @@ export class AgentyClient {
}): Promise<PreparedSession> {
const agent = await this.resolveAgent(options.agentRef);
const requestedModel = options.modelRef ? await this.resolveModel(options.modelRef) : undefined;
let session = options.newSession ? null : await this.getLastSessionByAgent(agent.slug);
let session = options.newSession ? null : await this.getLastSessionByAgent(agent.code);
if (!session) {
const model = requestedModel ?? await this.resolveAgentModel(agent);
session = await this.createSession(
agent.slug,
agent.code,
model,
options.reasoningEffort ?? agent.defaultReasoningEffort ?? "off",
);
return { agent, model, session };
}

if (requestedModel) {
const matchesCurrent = session.currentModel?.providerSlug === requestedModel.providerSlug &&
session.currentModel.modelSlug === requestedModel.slug;
const matchesCurrent = session.currentModel?.providerCode === requestedModel.providerCode &&
session.currentModel.modelCode === requestedModel.code;
if (!matchesCurrent) {
session = await this.setSessionModel(session.id, requestedModel);
}
Expand All @@ -293,7 +293,7 @@ export class AgentyClient {

if (session.currentModel) {
const model = await this.resolveModel(
`${session.currentModel.providerSlug}/${session.currentModel.modelSlug}`,
`${session.currentModel.providerCode}/${session.currentModel.modelCode}`,
);
return { agent, model, session };
}
Expand All @@ -305,7 +305,7 @@ export class AgentyClient {

private async resolveAgentModel(agent: AgentDto): Promise<ModelDto> {
if (agent.defaultModel) {
return this.resolveModel(`${agent.defaultModel.providerSlug}/${agent.defaultModel.modelSlug}`);
return this.resolveModel(`${agent.defaultModel.providerCode}/${agent.defaultModel.modelCode}`);
}
return this.getDefaultModel();
}
Expand All @@ -314,7 +314,7 @@ export class AgentyClient {
function projectModel(provider: ModelProviderDto, model: CoreModelDto): ModelDto {
return {
...model,
providerSlug: provider.slug,
providerCode: provider.code,
providerName: provider.name,
};
}
Expand Down Expand Up @@ -386,14 +386,14 @@ function requireSession(session: ChatSessionDto | null, operation: string): Chat
return normalizeSession(session);
}

function findProjectedModel(provider: ModelProviderDto | null, modelSlug: string): ModelDto {
function findProjectedModel(provider: ModelProviderDto | null, modelCode: string): ModelDto {
if (!provider) {
throw new Error("core returned an empty provider while adding a model");
}
const normalizedProvider = normalizeProvider(provider);
const model = normalizedProvider.models.find((candidate) => candidate.slug === modelSlug);
const model = normalizedProvider.models.find((candidate) => candidate.code === modelCode);
if (!model) {
throw new Error(`core did not return model ${normalizedProvider.slug}/${modelSlug}`);
throw new Error(`core did not return model ${normalizedProvider.code}/${modelCode}`);
}
return projectModel(normalizedProvider, model);
}
Expand Down
Loading
Loading