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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-loft",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "多个 agent 的工作台:把本机 Claude Code / Codex 放到一处统一管理,用飞书和 Web 页面随时驱动——会话、审批、模型切换都共用",
6
6
  "keywords": [
@@ -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;
@@ -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 = S.agents.filter((a) => a.enabled).map((a) => a.id);
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 = S.agents.filter((a) => a.enabled).map((a) => a.id);
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 = S.agents.filter((a) => a.enabled).length > 1;
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 = S.agents.filter((a) => a.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 ?? S.agents.find((a) => a.enabled)?.id ?? "claude";
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
- const close = modal({ title: "设置", wide: true, body: h("div", { class: "settings" }, nav, panel) });
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.replaceChildren(h("h4", {}, "服务端版本", h("span", { class: "grow" }), h("span", { class: "spinner" })));
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.replaceChildren(h("h4", {}, "服务端版本"), h("div", { class: "error-text" }, err.message));
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
- box.replaceChildren(
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.replaceChildren(h("h4", {}, "运行环境", h("span", { class: "grow" }), h("span", { class: "spinner" })));
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.replaceChildren(h("h4", {}, "运行环境", h("span", { class: "grow" }), recheck), h("div", { class: "error-text" }, err.message));
3789
+ fill(box, h("h4", {}, "运行环境", h("span", { class: "grow" }), recheck), h("div", { class: "error-text" }, err.message));
3776
3790
  return;
3777
3791
  }
3778
- box.replaceChildren(
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
+ }