agent-loft 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/health.ts +14 -0
- package/src/channels/web/api.ts +3 -1
- package/src/main.ts +3 -0
- package/web/app.js +36 -14
- package/web/style.css +19 -0
package/package.json
CHANGED
package/src/agents/health.ts
CHANGED
|
@@ -134,6 +134,20 @@ async function codexHealth(): Promise<AgentHealth> {
|
|
|
134
134
|
|
|
135
135
|
let cache: { at: number; value: AgentHealth[] } | null = null;
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* 不等体检、立刻给出的「这个 agent 能不能用」:用最近一次体检的结果(多旧都算),
|
|
139
|
+
* 结果超过 5 分钟就顺手在后台再查一次。还没查过返回 null(当作能用,不挡人)。
|
|
140
|
+
* 新对话里只列出能用的 agent,没装、没登录的不出现。
|
|
141
|
+
*/
|
|
142
|
+
export function agentAvailability(id: AgentHealth["id"]): { available: boolean; reason?: string } | null {
|
|
143
|
+
if (!cache || Date.now() - cache.at > 5 * 60_000) void agentHealth().catch(() => {});
|
|
144
|
+
const h = cache?.value.find((a) => a.id === id);
|
|
145
|
+
if (!h) return null;
|
|
146
|
+
if (!h.installed) return { available: false, reason: "未安装" };
|
|
147
|
+
if (h.loggedIn === false) return { available: false, reason: "未登录" };
|
|
148
|
+
return { available: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
/** 装了 / 登录了什么之后,下次体检不用缓存 */
|
|
138
152
|
export function invalidateHealth(): void {
|
|
139
153
|
cache = null;
|
package/src/channels/web/api.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { getConversation, updateConversation } from "../../core/store.js";
|
|
|
12
12
|
import { resolveUploadFile, saveBuffer, sniff } from "../../core/uploads.js";
|
|
13
13
|
import type { SavedAttachment } from "../../core/types.js";
|
|
14
14
|
import { listAllowlist, setAllowlist } from "../../agents/claude/permissions.js";
|
|
15
|
-
import { agentHealth, invalidateHealth } from "../../agents/health.js";
|
|
15
|
+
import { agentAvailability, agentHealth, invalidateHealth } from "../../agents/health.js";
|
|
16
16
|
import { installCodex, upgrade, useInstalledCodex, versionInfo } from "../../core/upgrade.js";
|
|
17
17
|
import { feishuBots } from "../feishu/index.js";
|
|
18
18
|
import { beginRegistration, initRegistration, pollRegistration, qrSvg } from "../feishu/register.js";
|
|
@@ -317,6 +317,8 @@ export async function handleApi(req: IncomingMessage, res: ServerResponse, url:
|
|
|
317
317
|
capabilities: d?.capabilities ?? {},
|
|
318
318
|
// 输入框模型列表里显示哪些,空 = 全部
|
|
319
319
|
models: config().agents[id].models.filter((m) => typeof m === "string"),
|
|
320
|
+
// 没装 / 没登录:新对话里不出现(null = 还没查过,当作能用)
|
|
321
|
+
...(agentAvailability(id) ?? {}),
|
|
320
322
|
};
|
|
321
323
|
}),
|
|
322
324
|
bots: config().feishu.bots.map((b) => ({ id: b.id, name: b.name, agents: b.agents, enabled: b.enabled })),
|
package/src/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { flushStore, getConversation, isExpired } from "./core/store.js";
|
|
|
5
5
|
import { sweepUploads } from "./core/uploads.js";
|
|
6
6
|
import { ClaudeDriver } from "./agents/claude/driver.js";
|
|
7
7
|
import { CodexDriver } from "./agents/codex/driver.js";
|
|
8
|
+
import { agentHealth } from "./agents/health.js";
|
|
8
9
|
import { startFeishu, stopFeishu } from "./channels/feishu/index.js";
|
|
9
10
|
import { startWeb, stopWeb } from "./channels/web/server.js";
|
|
10
11
|
import { home } from "./home.js";
|
|
@@ -40,6 +41,8 @@ async function main(): Promise<void> {
|
|
|
40
41
|
|
|
41
42
|
startFeishu();
|
|
42
43
|
if (config().web.enabled) await startWeb();
|
|
44
|
+
// 先查一遍 agent 装没装、登没登录,网页打开时新对话里就只列出能用的
|
|
45
|
+
void agentHealth().catch(() => {});
|
|
43
46
|
|
|
44
47
|
console.log(`[启动] workspace=${config().workspaceRoot}`);
|
|
45
48
|
if (config().workspaceRoot === process.cwd()) {
|
package/web/app.js
CHANGED
|
@@ -265,6 +265,8 @@ const ui = {};
|
|
|
265
265
|
* 桌面端收不到 Web Push,系统通知、Dock 角标、菜单栏的待处理数都交给它。
|
|
266
266
|
*/
|
|
267
267
|
const desktop = window.agentLoftDesktop ?? null;
|
|
268
|
+
// 桌面端里:顶栏、侧栏顶部可以按住拖动窗口(样式见 style.css 的 .in-desktop)
|
|
269
|
+
if (desktop) document.documentElement.classList.add("in-desktop");
|
|
268
270
|
|
|
269
271
|
// ---------- WebSocket ----------
|
|
270
272
|
|
|
@@ -528,7 +530,7 @@ async function boot() {
|
|
|
528
530
|
S.quotaRefreshMin = data.quotaRefreshMin ?? 10;
|
|
529
531
|
S.convs = new Map(data.conversations.map((c) => [c.key, c]));
|
|
530
532
|
S.pending = new Map(data.pending.map((i) => [i.id, i]));
|
|
531
|
-
const enabled =
|
|
533
|
+
const enabled = usableAgents().map((a) => a.id);
|
|
532
534
|
if (!S.draftAgent || !enabled.includes(S.draftAgent)) S.draftAgent = enabled[0] ?? null;
|
|
533
535
|
renderLayout();
|
|
534
536
|
reportDesktop();
|
|
@@ -1113,8 +1115,18 @@ function currentCwd() {
|
|
|
1113
1115
|
return c ? c.cwd : (S.draftCwd ?? S.workspaceRoot ?? "");
|
|
1114
1116
|
}
|
|
1115
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* 新对话能选的 agent:启用了、并且服务端查过是装好登录了的(available 为 false 的不出现)。
|
|
1120
|
+
* 一个能用的都没有时退回到全部启用的,至少能看到报错,而不是什么都选不了。
|
|
1121
|
+
*/
|
|
1122
|
+
function usableAgents() {
|
|
1123
|
+
const enabled = S.agents.filter((a) => a.enabled);
|
|
1124
|
+
const usable = enabled.filter((a) => a.available !== false);
|
|
1125
|
+
return usable.length ? usable : enabled;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1116
1128
|
function pickAgentMenu(anchor) {
|
|
1117
|
-
const allowed =
|
|
1129
|
+
const allowed = usableAgents().map((a) => a.id);
|
|
1118
1130
|
if (allowed.length < 2) return;
|
|
1119
1131
|
if (ui.menu?.dataset.for === "agent") return closeMenu();
|
|
1120
1132
|
const pop = showPopover(anchor, { align: "left" });
|
|
@@ -1205,7 +1217,7 @@ function renderContext() {
|
|
|
1205
1217
|
if (!ui.ctxBar) return;
|
|
1206
1218
|
const c = S.current ? S.convs.get(S.current) : null;
|
|
1207
1219
|
const cwd = currentCwd();
|
|
1208
|
-
const multi =
|
|
1220
|
+
const multi = usableAgents().length > 1;
|
|
1209
1221
|
// 会话里不能换 agent(两个 agent 的会话互不相通),只显示;新对话里可以选
|
|
1210
1222
|
const agentId = c ? c.agent : S.draftAgent;
|
|
1211
1223
|
const agentEl = c || !multi
|
|
@@ -1437,7 +1449,7 @@ function newDraft() {
|
|
|
1437
1449
|
}
|
|
1438
1450
|
|
|
1439
1451
|
function renderEmpty() {
|
|
1440
|
-
const enabled =
|
|
1452
|
+
const enabled = usableAgents();
|
|
1441
1453
|
const cwd = S.draftCwd ?? S.workspaceRoot ?? "";
|
|
1442
1454
|
fill(ui.msgInner,
|
|
1443
1455
|
h(
|
|
@@ -2382,7 +2394,7 @@ function oneLine(text, max = 80) {
|
|
|
2382
2394
|
|
|
2383
2395
|
function currentAgentId() {
|
|
2384
2396
|
const c = S.current ? S.convs.get(S.current) : null;
|
|
2385
|
-
return c?.agent ?? S.draftAgent ??
|
|
2397
|
+
return c?.agent ?? S.draftAgent ?? usableAgents()[0]?.id ?? "claude";
|
|
2386
2398
|
}
|
|
2387
2399
|
|
|
2388
2400
|
async function loadCommands(agent) {
|
|
@@ -2593,14 +2605,14 @@ async function switchAgent(id) {
|
|
|
2593
2605
|
await doAction("agent", id);
|
|
2594
2606
|
}
|
|
2595
2607
|
|
|
2596
|
-
function modal({ title, body, foot, wide }) {
|
|
2608
|
+
function modal({ title, body, foot, wide, tall }) {
|
|
2597
2609
|
const close = () => overlay.remove();
|
|
2598
2610
|
const overlay = h(
|
|
2599
2611
|
"div",
|
|
2600
2612
|
{ class: "overlay", onclick: (e) => e.target === overlay && close() },
|
|
2601
2613
|
h(
|
|
2602
2614
|
"div",
|
|
2603
|
-
{ class: `modal${wide ? " wide" : ""}` },
|
|
2615
|
+
{ class: `modal${wide ? " wide" : ""}${tall ? " tall" : ""}` },
|
|
2604
2616
|
h("div", { class: "modal-head" }, h("h3", {}, title), h("button", { class: "icon-btn", onclick: close }, icon("x"))),
|
|
2605
2617
|
body,
|
|
2606
2618
|
foot ?? null,
|
|
@@ -3045,7 +3057,8 @@ async function deleteConv(key = S.current) {
|
|
|
3045
3057
|
async function openSettings() {
|
|
3046
3058
|
const nav = h("div", { class: "settings-nav" });
|
|
3047
3059
|
const panel = h("div", { class: "settings-body" });
|
|
3048
|
-
|
|
3060
|
+
// 设置弹窗高度固定,切标签页时不跟着内容一伸一缩
|
|
3061
|
+
const close = modal({ title: "设置", wide: true, tall: true, body: h("div", { class: "settings" }, nav, panel) });
|
|
3049
3062
|
let cfg;
|
|
3050
3063
|
try {
|
|
3051
3064
|
cfg = await api("/api/config");
|
|
@@ -3232,12 +3245,12 @@ function field(label, input, hint) {
|
|
|
3232
3245
|
function serverVersionSection() {
|
|
3233
3246
|
const box = h("div", { class: "section" });
|
|
3234
3247
|
const load = async (refresh) => {
|
|
3235
|
-
box
|
|
3248
|
+
fill(box, h("h4", {}, "服务端版本", h("span", { class: "grow" }), h("span", { class: "spinner" })));
|
|
3236
3249
|
let v;
|
|
3237
3250
|
try {
|
|
3238
3251
|
v = await api(`/api/server/version${refresh ? "?refresh=1" : ""}`);
|
|
3239
3252
|
} catch (err) {
|
|
3240
|
-
box
|
|
3253
|
+
fill(box, h("h4", {}, "服务端版本"), h("div", { class: "error-text" }, err.message));
|
|
3241
3254
|
return;
|
|
3242
3255
|
}
|
|
3243
3256
|
const recheck = h("button", { class: "btn small", onclick: () => load(true) }, "检查更新");
|
|
@@ -3269,7 +3282,8 @@ function serverVersionSection() {
|
|
|
3269
3282
|
"升级",
|
|
3270
3283
|
)
|
|
3271
3284
|
: null;
|
|
3272
|
-
|
|
3285
|
+
fill(
|
|
3286
|
+
box,
|
|
3273
3287
|
h("h4", {}, "服务端版本", h("span", { class: "grow" }), upgradeBtn, recheck),
|
|
3274
3288
|
h("div", { class: "health-meta" }, `当前 ${v.version} · ${status}`),
|
|
3275
3289
|
v.updateAvailable && !v.canUpgrade && v.reason ? h("div", { class: "health-meta" }, v.reason) : null,
|
|
@@ -3767,15 +3781,23 @@ function agentHealthSection() {
|
|
|
3767
3781
|
const box = h("div", { class: "section" });
|
|
3768
3782
|
const load = async (refresh) => {
|
|
3769
3783
|
const recheck = h("button", { class: "btn small", onclick: () => load(true) }, "重新检查");
|
|
3770
|
-
box
|
|
3784
|
+
fill(box, h("h4", {}, "运行环境", h("span", { class: "grow" }), h("span", { class: "spinner" })));
|
|
3771
3785
|
let data;
|
|
3772
3786
|
try {
|
|
3773
3787
|
data = await api(`/api/agents/health${refresh ? "?refresh=1" : ""}`);
|
|
3774
3788
|
} catch (err) {
|
|
3775
|
-
box
|
|
3789
|
+
fill(box, h("h4", {}, "运行环境", h("span", { class: "grow" }), recheck), h("div", { class: "error-text" }, err.message));
|
|
3776
3790
|
return;
|
|
3777
3791
|
}
|
|
3778
|
-
|
|
3792
|
+
// 体检结果顺手同步到新对话的 agent 列表:刚装好 / 登录好的立刻能选,不行的立刻消失
|
|
3793
|
+
for (const a of data.agents) {
|
|
3794
|
+
const known = S.agents.find((x) => x.id === a.id);
|
|
3795
|
+
if (known) known.available = a.installed && a.loggedIn !== false;
|
|
3796
|
+
}
|
|
3797
|
+
if (!usableAgents().some((a) => a.id === S.draftAgent)) S.draftAgent = usableAgents()[0]?.id ?? null;
|
|
3798
|
+
if (!S.current) renderEmpty();
|
|
3799
|
+
fill(
|
|
3800
|
+
box,
|
|
3779
3801
|
h("h4", {}, "运行环境", h("span", { class: "grow" }), recheck),
|
|
3780
3802
|
...data.agents.map((a) => {
|
|
3781
3803
|
const ok = a.installed && a.loggedIn === true;
|
package/web/style.css
CHANGED
|
@@ -2300,6 +2300,11 @@ button.ctx-item:hover {
|
|
|
2300
2300
|
max-width: 860px;
|
|
2301
2301
|
}
|
|
2302
2302
|
|
|
2303
|
+
/* 设置弹窗:固定高度 */
|
|
2304
|
+
.modal.tall {
|
|
2305
|
+
height: 86vh;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2303
2308
|
.modal-head {
|
|
2304
2309
|
display: flex;
|
|
2305
2310
|
align-items: center;
|
|
@@ -2747,6 +2752,10 @@ button.list-item:hover {
|
|
|
2747
2752
|
align-items: center;
|
|
2748
2753
|
}
|
|
2749
2754
|
|
|
2755
|
+
.modal.tall {
|
|
2756
|
+
height: 92vh;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2750
2759
|
.modal,
|
|
2751
2760
|
.modal.wide {
|
|
2752
2761
|
max-width: none;
|
|
@@ -3214,3 +3223,13 @@ button.list-item:hover {
|
|
|
3214
3223
|
background: var(--code-bg);
|
|
3215
3224
|
font-size: 12px;
|
|
3216
3225
|
}
|
|
3226
|
+
|
|
3227
|
+
/* 桌面端(Agent Loft for macOS)里:顶栏和侧栏顶部按住能拖动窗口,里面的按钮照常能点 */
|
|
3228
|
+
.in-desktop .topbar,
|
|
3229
|
+
.in-desktop .side-top {
|
|
3230
|
+
-webkit-app-region: drag;
|
|
3231
|
+
}
|
|
3232
|
+
.in-desktop .topbar :is(button, a, input, select, textarea, label, .conv-name),
|
|
3233
|
+
.in-desktop .side-top :is(button, a, input, select, textarea, label) {
|
|
3234
|
+
-webkit-app-region: no-drag;
|
|
3235
|
+
}
|