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 .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"runtimeArgs": [
"./frontend/e2e/wails-server.mjs"
],
"port": 34123
"port": 8080
}
]
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ frontend/coverage/
frontend/test-results/
frontend/playwright-report/

# CI 跑 scripts/ 下的 unittest 会留下字节码
__pycache__/

# 可再生的产物:imagegen 设计稿与工具自动生成的 repowiki
output/
.qoder/
Expand Down
20 changes: 17 additions & 3 deletions frontend/src/components/AgentManageRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,25 @@ describe("AgentManageRow", () => {
}
});

it("distinguishes an unconfigured Agent from a configured one", () => {
it("names which piece is missing rather than one shared status word", () => {
renderRow({ configured: false, provider: null, profileId: null, model: null, baseUrl: null }, "");
expect(screen.queryByText("未记录")).toBeNull();
expect(screen.getByText("未配置", { selector: ".agent-manage-state" })).toBeTruthy();
expect(screen.getByText("未绑定")).toBeTruthy();
// Each token names its own field. A shared "未绑定" on both, plus a separate
// "未配置" badge, said the same thing three times without saying which of
// the two was actually absent.
expect(screen.getByTitle("配置模板").textContent).toBe("无 Profile");
expect(screen.getByTitle("模型服务").textContent).toBe("无 Provider");
expect(screen.queryByText("未配置")).toBeNull();
expect(screen.queryByText("未绑定")).toBeNull();
});

it("keeps the token order fixed when a field is absent", () => {
// The Provider token used to be dropped when empty, sliding model and
// version left so the same field sat in a different slot per row.
renderRow({ provider: null, model: "m-1", version: "1.0.0" }, "prod");
const order = [...document.querySelectorAll(".agent-manage-meta .agent-manage-pill")]
.map((pill) => pill.getAttribute("title"));
expect(order).toEqual(["配置模板", "模型服务", "模型", "版本"]);
});

it("flags a version behind the locked one", () => {
Expand Down
64 changes: 24 additions & 40 deletions frontend/src/components/AgentManageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,6 @@ const npmPackages: Record<string, string> = {
"kilo-cli": "@kilocode/cli",
};

const AGENT_ACCENTS: Record<string, "blue" | "green" | "orange"> = {
codex: "blue",
"claude-code": "orange",
opencode: "green",
"kilo-cli": "blue",
aider: "orange",
};

function agentAccent(agentId: string): "blue" | "green" | "orange" {
return AGENT_ACCENTS[agentId] || "blue";
}

/** -1, 0 or 1 comparing dotted numeric versions; non-numeric parts sort last. */
export function compareVersions(left: string, right: string): number {
const parse = (value: string) => value.split(/[.+-]/).map((part) => Number.parseInt(part, 10));
Expand Down Expand Up @@ -132,8 +120,10 @@ export function AgentManageRow({
const profileLabel = profileName || status.profileId || "";
const model = profile?.model || status.model || status.detected?.model || "";
const baseUrl = status.baseUrl || status.detected?.baseUrl || "";
const hasConfiguration = Boolean(providerName || profileLabel || model || baseUrl);
const statusLabel = failure ? t("失败") : !status.installed ? t("未安装") : !hasConfiguration ? t("未配置") : "";
// No "not configured" state here: the Profile and Provider tokens already name
// whichever piece is absent, so a third word for the same condition only adds
// a term the user has to map back onto them.
const statusLabel = failure ? t("失败") : !status.installed ? t("未安装") : "";

// installed is true only when the Agent's command resolved on the managed
// PATH, so it is already the precise "there is something to launch" signal.
Expand Down Expand Up @@ -164,7 +154,7 @@ export function AgentManageRow({
};

return (
<div className="agent-manage-row" data-accent={agentAccent(agentId)} data-testid={`agent-${agentId}`}>
<div className="agent-manage-row" data-testid={`agent-${agentId}`}>
<div className="agent-manage-summary">
<div className="agent-manage-identity">
<span className="agent-icon" title={agentTagline(agentId, t) || undefined}>
Expand All @@ -175,15 +165,17 @@ export function AgentManageRow({
{failure ? <small className="agent-manage-note is-error">{failure}</small> : null}
</span>
</div>
{/* Right-aligned, in a fixed order, with the Profile and Provider slots
always rendered. Dropping an absent Provider slid every later token
left, so the same field sat in a different position from row to row
and the strip could not be read down the column. */}
<div className="agent-manage-meta" aria-label={t("状态")}>
{providerName ? (
<span className="agent-manage-pill" title={t("模型服务")}>
<i aria-hidden="true" />
{providerName}
</span>
) : null}
<span className={`agent-manage-pill${profileLabel ? "" : " is-muted"}`} title={t("配置模板")}>
{profileLabel || t("未绑定")}
{profileLabel || t("无 Profile")}
</span>
<span className={`agent-manage-pill${providerName ? "" : " is-muted"}`} title={t("模型服务")}>
{providerName ? <i aria-hidden="true" /> : null}
{providerName || t("无 Provider")}
</span>
{model ? (
<span className="agent-manage-pill agent-manage-model" title={t("模型")}>
Expand All @@ -199,15 +191,16 @@ export function AgentManageRow({
</div>
</div>
<div className="agent-manage-actions">
{!canLaunch ? (
<Link
className="button button-secondary"
to={`/agents/${agentId}`}
title={t("编辑这个 Agent 关联的 Profile")}
>
{t("配置")}
</Link>
) : null}
{/* Always in the row, not only when the Agent cannot launch. Configuring
an installed Agent was previously reachable only by opening <details>,
which made the common case the hidden one. */}
<Link
className="button button-secondary"
to={`/agents/${agentId}`}
title={t("编辑这个 Agent 关联的 Profile")}
>
{t("配置")}
</Link>
{npmAgents.has(agentId) ? (
<button className="button button-secondary" type="button" onClick={() => void update()} disabled={updating || launching} title={t("执行 npm update")}>
{t("更新")}
Expand All @@ -229,15 +222,6 @@ export function AgentManageRow({
<details className="agent-manage-details">
<summary>{t("详情")}</summary>
<div className="agent-manage-details-body">
{canLaunch ? (
<Link
className="agent-manage-detail-action"
to={`/agents/${agentId}`}
title={t("编辑这个 Agent 关联的 Profile")}
>
{t("配置")}
</Link>
) : null}
{providerName ? (
<div><small>{t("模型服务")}</small><span>{providerName}</span></div>
) : null}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/DesktopAppSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ describe("DesktopAppSection", () => {
fireEvent.click(screen.getByRole("button", { name: "更新" }));
await waitFor(() => expect(bridge.openDesktopAgentInstaller).toHaveBeenCalledTimes(1));
expect(screen.getByText("官方安装器已启动")).toBeTruthy();
expect(view.container.textContent).toContain("版本26.727.51351");
// The version is a token now, so its heading lives in title= rather than
// beside the value. Assert the labelled element, not the concatenation.
expect(screen.getByTitle("版本").textContent).toBe("26.727.51351");
});

it("does not claim an app was found when inspection is unavailable", () => {
Expand Down
50 changes: 29 additions & 21 deletions frontend/src/components/DesktopAppSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,27 +89,35 @@ export function DesktopAppSection({ app: desktopApp, onChanged, onSetup, onConfi
</button>
</div>
) : null}
<div className="desktop-app-identity">
<span className="desktop-app-icon"><AgentIcon agentId="codex" size={20} /></span>
<span>
<strong>{desktopApp.name}</strong>
</span>
</div>
<div className="desktop-app-fact">
<small>Provider</small>
<span title={profile?.provider || undefined}>{providerName || profile?.provider || t("未绑定")}</span>
</div>
<div className="desktop-app-fact">
<small>Profile</small>
<span title={profile?.id || desktopApp.profileId || undefined}>{profile?.label || desktopApp.profileId || t("未绑定")}</span>
</div>
<div className="desktop-app-fact">
<small>{t("模型")}</small>
<span title={model || profile?.model || undefined}>{model || profile?.model || t("未记录")}</span>
</div>
<div className="desktop-app-fact">
<small>{t("版本")}</small>
<span>{desktopApp.version || t("未知")}</span>
<div className="desktop-app-summary">
<div className="desktop-app-identity">
<span className="desktop-app-icon"><AgentIcon agentId="codex" size={20} /></span>
<span>
<strong>{desktopApp.name}</strong>
</span>
</div>
{/* Same token strip as the CLI rows. Four labelled fact columns made
this card twice the height of the rows beneath it for the same
four values. */}
<div className="desktop-app-meta">
<span className={`agent-manage-pill${profile?.label || desktopApp.profileId ? "" : " is-muted"}`} title={t("配置模板")}>
{profile?.label || desktopApp.profileId || t("无 Profile")}
</span>
<span className={`agent-manage-pill${providerName || profile?.provider ? "" : " is-muted"}`} title={t("模型服务")}>
{providerName || profile?.provider ? <i aria-hidden="true" /> : null}
{providerName || profile?.provider || t("无 Provider")}
</span>
{model || profile?.model ? (
<span className="agent-manage-pill agent-manage-model" title={t("模型")}>
{model || profile?.model}
</span>
) : null}
{desktopApp.version ? (
<span className="agent-manage-pill agent-manage-version" title={t("版本")}>
{desktopApp.version}
</span>
) : null}
</div>
</div>
<div className="desktop-app-actions">
{desktopApp.installed ? (
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/icons/agents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ describe("AgentIcon", () => {
});

it("records auditable rights for every redistributed image asset", () => {
for (const id of ["codex"]) {
// Every id whose mark is a real image, not a generic symbol. Asserting the
// set rather than one example is what makes an unregistered mark fail here:
// shipping artwork without a source, licence and hash is the defect.
const assetIds = AGENT_ICON_IDS.filter((id) => agentMarkKind(id) === "asset");
expect(assetIds.sort()).toEqual(["claude-code", "codex", "cursor", "kilo-cli", "opencode"]);
for (const id of assetIds) {
const rights = agentMarkRights(id);
expect(agentMarkKind(id)).toBe("asset");
expect(rights?.license).toBe("MIT");
Expand Down
29 changes: 23 additions & 6 deletions frontend/src/components/icons/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@
* marks are not recoloured or restyled.
*/
import {
Blocks,
Bot,
Braces,
GitBranch,
MousePointer2,
type LucideIcon
} from "lucide-react";

import { sourceTranslate, type Translate, type TranslationKey } from "../../i18n";

import assetRightsManifest from "./asset-rights.json";
import claudeCodeMark from "./assets/claude-code.svg";
import codexMark from "./assets/codex.svg";
import cursorMark from "./assets/cursor.svg";
import kiloCliMark from "./assets/kilo-cli.svg";
import opencodeMark from "./assets/opencode.svg";

type AssetRights = (typeof assetRightsManifest.assets)[keyof typeof assetRightsManifest.assets];
Expand All @@ -46,9 +46,26 @@ const MARKS: Record<string, Mark> = {
source: assetRightsManifest.assets.opencode.source,
rights: assetRightsManifest.assets.opencode,
},
"claude-code": { kind: "generic", Icon: Braces, source: GENERIC_SOURCE },
cursor: { kind: "generic", Icon: MousePointer2, source: GENERIC_SOURCE },
"kilo-cli": { kind: "generic", Icon: Blocks, source: GENERIC_SOURCE },
"claude-code": {
kind: "asset",
src: claudeCodeMark,
source: assetRightsManifest.assets["claude-code"].source,
rights: assetRightsManifest.assets["claude-code"],
},
cursor: {
kind: "asset",
src: cursorMark,
source: assetRightsManifest.assets.cursor.source,
rights: assetRightsManifest.assets.cursor,
},
"kilo-cli": {
kind: "asset",
src: kiloCliMark,
source: assetRightsManifest.assets["kilo-cli"].source,
rights: assetRightsManifest.assets["kilo-cli"],
},
// Aider has no mark in lobe-icons, so it keeps a generic symbol rather than a
// vendor favicon copied in without an auditable redistribution basis.
aider: { kind: "generic", Icon: GitBranch, source: GENERIC_SOURCE },
};

Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/icons/asset-rights.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "7cfa6e9d6726f7c9fa26c7d9aef0dfec52d20a137380454340f30f12ccbfd302"
},
"claude-code": {
"file": "assets/claude-code.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/claude.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "365a70a7eb3956d9b9a96086058ebe04e1dbd8e291a756ad964e8a283fbd6d38"
},
"cursor": {
"file": "assets/cursor.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/cursor.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "0cb51bddf264ae108926fd554c063ef40fc1aac3c5c921ddb39ad184e4e5d0ef"
},
"kilo-cli": {
"file": "assets/kilo-cli.svg",
"source": "https://github.com/lobehub/lobe-icons/blob/master/packages/static-svg/icons/kilocode.svg",
"license": "MIT",
"licenseSource": "licenses/lobehub/LICENSE",
"copyrightOwner": "LobeHub contributors",
"sha256": "a2aef0e0bb992ebe768bfb8c3a3ab6ff2c2638448b09dd88db27877de1c07a22"
}
}
}
1 change: 1 addition & 0 deletions frontend/src/components/icons/assets/claude-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/components/icons/assets/cursor.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/components/icons/assets/kilo-cli.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 0 additions & 60 deletions frontend/src/components/icons/assets/openclaw.svg

This file was deleted.

Loading
Loading