agent-loft 0.2.26 → 0.2.27

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/README.md CHANGED
@@ -47,7 +47,7 @@ Logo 就是这个意思:一大两小几颗星(多个 agent)站在同一张
47
47
  - 新对话页:点大标题里的目录名,在目录树里逐层选工作目录;输入框上方选 agent,右下角选模型和思考程度。会话进行中用 `/agent` 切换 agent,模型和思考程度随时可换。
48
48
  - 输入框里的小圆环显示上下文窗口占用;模型按钮显示实际的模型和思考程度(如「Opus 5.5 high」)。
49
49
  - 滚动和 ChatGPT 一样:发出的问题顶到最上面、下面留白,回复长到屏幕底部才跟着往下滚;回复过程中自己滚动了就不再自动滚,下次发消息再恢复。
50
- - 定时发送:输入框左下角的时钟按钮,选好时间(有「额度重置后」的快捷选项)由服务端到点发出,关掉页面也会发。适合额度用完、要等刷新才能接着干活的时候。
50
+ - 定时发送:右上角「更多」→「定时发送」,默认排在 5 小时额度重置后 5 分钟(弹窗里提示 5 小时 / 7 天额度的重置时间),由服务端到点发出,关掉页面也会发。适合额度用完、要等刷新才能接着干活的时候。
51
51
  - 侧栏分「最近」和「项目」两组:Web 会话按工作目录归类(目录可以拖动排序,右键可置顶),飞书机器人各占一行(右键看历史会话);分叉出来的会话用虚线挂在原会话下面;会话标题由 agent 自动生成,双击重命名。
52
52
  - 侧栏底部显示 Claude / Codex 的订阅额度(5 小时 / 7 天窗口,以及多久后重置)。
53
53
  - 「添加到主屏幕」后当 App 用;切到后台再回来会自动重连,补上错过的内容。
@@ -56,7 +56,7 @@ Logo 就是这个意思:一大两小几颗星(多个 agent)站在同一张
56
56
  - **授权**:允许一次 / 本会话全部允许 / 总是允许(按钮上写明放行范围)/ 拒绝。`/autoedit on` 让 Claude Code 在这条会话里改工作目录内的文件不再询问(Bash 仍询问)。超时一律按拒绝处理。
57
57
  - **设置都在网页上**:工作目录、会话闲置过期时间、局域网访问和端口、各 agent 的默认模型和思考程度、可选模型、Codex 沙箱、飞书机器人、配对批准、Claude 的「总是允许」名单、通知设备、登录设备管理、用户管理、服务端升级。
58
58
  - **多用户**(信任型):管理员在 设置 → 用户 里给别人建账号。每个人只看得到自己的对话、授权和通知,设置只有管理员能改,见下面「安全」。
59
- - **聊天模式 / 工作模式**:输入框右下角的「💬 聊天 / 🛠 工作」,或者发 `/chat`、`/work`,同一条对话随时切换、上下文接着。聊天模式换成精简的系统提示,只留联网搜索,不加载技能、MCP、插件,不执行命令、不读写本机文件(文本附件的内容直接放进消息),每轮的固定开销小得多:实测一句话 Claude 约 1.6 万 → 2 千 token,Codex 约 2 万 → 1.2 万(Codex 的基础指令改不了)。适合日常聊天、查资料;要动手干活时切回工作模式。切换后第一轮要重建提示缓存,会贵一些;在工作模式里读过的大段内容还留在历史里。
59
+ - **聊天模式 / 工作模式**:右上角的「💬 聊天 / 🛠 工作」,或者发 `/chat`、`/work`,同一条对话随时切换、上下文接着。聊天模式换成精简的系统提示,只留联网搜索,不加载技能、MCP、插件,不执行命令、不读写本机文件(文本附件的内容直接放进消息),每轮的固定开销小得多:实测一句话 Claude 约 1.6 万 → 2 千 token,Codex 约 2 万 → 1.2 万(Codex 的基础指令改不了)。适合日常聊天、查资料;要动手干活时切回工作模式。切换后第一轮要重建提示缓存,会贵一些;在工作模式里读过的大段内容还留在历史里。
60
60
  - **记忆**:像 ChatGPT 的记忆,设置 → 记忆。「关于我」加上一条条记住的事,每次对话都带上;agent 在聊天里遇到长期有用的信息会自己用 `remember` 记一条(回复里会说),记错了用 `forget` 删。**自动整理**:把上次整理之后新说的话交给模型(不留会话记录的一次性调用),补充、更新或删掉过时的记忆,用户自己加的条目它不动。什么时候整理每个人自己定:每天一次(默认凌晨 4 点,把当天的对话合起来一次整理,最省)/每段对话聊完后(停多久可选,改了什么会在对话里说一声)/只在点「现在整理」时/不整理;用哪个 agent 的哪个模型也能选,默认 Claude Haiku,没有 Claude 就用 Codex 的默认模型、思考程度 low。**参考以前的聊天**:agent 遇到「上次说的那个……」会用 `search_past_chats` 搜你以前的对话(关键词搜索,只有你自己的、没被删掉的对话;第一次用时会把已有对话收录进来)。两项都能在设置里单独关。每个用户一份,Claude 和 Codex、Web 和飞书共用。只用在有主人的对话里:你的 Web 对话,以及绑定了飞书账号后和机器人的私聊;飞书群聊不用。别人(包括管理员)看不到你的记忆,管理员只能清空它;删用户时记忆一起删,不转给管理员。
61
61
  - **飞书账号绑定**:设置 → 账号 → 飞书账号 →「生成绑定码」,再在飞书私聊机器人发 `/bind 绑定码`(10 分钟内有效,群里发不算)。绑定后这个私聊就是你的个人会话:用你的记忆,`/sessions` 只列你的会话,`/web` 只能接上你自己的 Web 会话,管理员在 Web 上也看不到它。必须本人在两边各操作一次,管理员不能替人绑定(只能解除),否则就能借飞书读到别人的记忆。
62
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-loft",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "type": "module",
5
5
  "description": "多个 agent 的工作台:把本机 Claude Code / Codex 放到一处统一管理,用飞书和 Web 页面随时驱动——会话、审批、模型切换都共用",
6
6
  "keywords": [
package/web/app.js CHANGED
@@ -38,6 +38,7 @@ const ICON = {
38
38
  stop: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg>',
39
39
  clip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.4 11.6-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg>',
40
40
  mic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5"/></svg>',
41
+ more: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>',
41
42
  clock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
42
43
  down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="m6 9 6 6 6-6"/></svg>',
43
44
  gear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></svg>',
@@ -683,6 +684,8 @@ function renderLayout() {
683
684
  h("div", { class: "side-resizer", title: "拖动调整宽度,双击恢复默认", onpointerdown: startSideResize, ondblclick: () => setSideWidth(null) }),
684
685
  );
685
686
  ui.topbar = h("div", { class: "topbar" });
687
+ // 聊天 / 工作切换在顶栏右上角;先建好,renderTopbar 每次把它放回去
688
+ ui.modeSlot = h("span", { class: "mode-slot" });
686
689
  ui.msgInner = h("div", { class: "messages-inner" });
687
690
  ui.spacer = h("div", { class: "messages-spacer", "aria-hidden": "true" });
688
691
  ui.messages = h("div", { class: "messages", tabindex: "-1" }, ui.msgInner, ui.spacer);
@@ -1290,10 +1293,22 @@ function renderTopbar() {
1290
1293
  fill(ui.topbar,
1291
1294
  h("button", { class: "icon-btn menu-btn", onclick: openSide, title: "会话列表" }, icon("menu")),
1292
1295
  h("div", { class: "top-title" }, c ? h("span", { class: "conv-name", title: convTitle(c) }, convTitle(c)) : null),
1296
+ ui.modeSlot,
1297
+ h("button", { class: "icon-btn top-more", title: "更多", onclick: (e) => openTopMore(e.currentTarget) }, icon("more")),
1293
1298
  );
1294
1299
  renderContext();
1295
1300
  }
1296
1301
 
1302
+ /** 顶栏右上角「更多」 */
1303
+ function openTopMore(anchor) {
1304
+ if (ui.menu?.anchor === anchor) return closeMenu();
1305
+ showMenu(anchor, [{ label: "定时发送…", onclick: () => openSchedule() }]);
1306
+ ui.menu.anchor = anchor;
1307
+ // 贴着按钮右边对齐,别超出屏幕
1308
+ const r = anchor.getBoundingClientRect();
1309
+ ui.menu.style.left = `${Math.max(8, r.right - ui.menu.offsetWidth)}px`;
1310
+ }
1311
+
1297
1312
  function baseName(p) {
1298
1313
  return (p ?? "").split(/[\\/]/).filter(Boolean).pop() || p || "";
1299
1314
  }
@@ -2380,7 +2395,6 @@ function renderComposer() {
2380
2395
  : null;
2381
2396
  ui.ctxBar = h("div", { class: "ctx-bar" });
2382
2397
  ui.modelSlot = h("span", { class: "model-slot" });
2383
- ui.modeSlot = h("span", { class: "mode-slot" });
2384
2398
  ui.ctxRing = h("span", { class: "ctx-ring-slot" });
2385
2399
  ui.chips = h("div", { class: "chips" });
2386
2400
  ui.schedBar = h("div", { class: "sched-bar", hidden: true });
@@ -2407,11 +2421,9 @@ function renderComposer() {
2407
2421
  "div",
2408
2422
  { class: "composer-row" },
2409
2423
  h("button", { class: "icon-btn add-btn", title: "添加附件", onclick: () => ui.fileInput.click() }, icon("plus")),
2410
- h("button", { class: "icon-btn add-btn", title: "定时发送", onclick: () => openSchedule() }, icon("clock")),
2411
2424
  ui.fileInput,
2412
2425
  h("span", { class: "spacer" }),
2413
2426
  ui.ctxRing,
2414
- ui.modeSlot,
2415
2427
  ui.modelSlot,
2416
2428
  ui.micBtn,
2417
2429
  ui.sendBtn,
@@ -2489,33 +2501,25 @@ function openSchedule(edit) {
2489
2501
  const text = h("textarea", { class: "sched-input", rows: 4, placeholder: "到点要发的内容" });
2490
2502
  text.value = edit ? edit.text : ui.input.value;
2491
2503
  const when = h("input", { type: "datetime-local", class: "sched-time" });
2492
- const setAt = (ms) => (when.value = toLocalInput(ms));
2493
- setAt(edit ? edit.at : Date.now() + 5 * 3600_000);
2494
- // 快捷时间:当前 agent 的额度窗口重置后一分钟,再加几个常用的
2504
+ // 当前 agent 的额度窗口(5 小时 / 7 天)什么时候重置:小字提示,默认排在 5 小时窗口重置后 5 分钟
2495
2505
  const agent = currentAgentId();
2496
- const presets = [];
2497
- for (const q of quotaState.data ?? []) {
2498
- if (q.agent !== agent) continue;
2499
- for (const w of q.windows) {
2500
- if (w.resetsAt && w.resetsAt > Date.now()) presets.push({ label: `${w.label}重置后(${fmtWhen(w.resetsAt + 60_000)})`, at: w.resetsAt + 60_000 });
2501
- }
2502
- }
2503
- const tomorrow9 = new Date();
2504
- tomorrow9.setDate(tomorrow9.getDate() + 1);
2505
- tomorrow9.setHours(9, 0, 0, 0);
2506
- presets.push(
2507
- { label: "1 小时后", at: Date.now() + 3600_000 },
2508
- { label: "3 小时后", at: Date.now() + 3 * 3600_000 },
2509
- { label: "5 小时后", at: Date.now() + 5 * 3600_000 },
2510
- { label: "明早 9:00", at: tomorrow9.getTime() },
2511
- );
2506
+ const windows = (quotaState.data ?? []).find((q) => q.agent === agent)?.windows ?? [];
2507
+ const resets = windows.filter((w) => /^(5h|7d)$/.test(w.short) && w.resetsAt > Date.now());
2508
+ const session = resets.find((w) => w.short === "5h");
2509
+ when.value = toLocalInput(edit ? edit.at : session ? session.resetsAt + 5 * 60_000 : Date.now() + 5 * 3600_000);
2512
2510
  const body = h(
2513
2511
  "div",
2514
2512
  { class: "modal-body sched-body" },
2515
2513
  text,
2516
- h("div", { class: "sched-presets" }, presets.map((p) => h("button", { class: "btn", onclick: () => setAt(p.at) }, p.label))),
2517
2514
  h("label", { class: "sched-row" }, h("span", {}, "发送时间"), when),
2518
- h("div", { class: "muted" }, "到点由服务端发出,关掉页面也会发。到点时 agent 还在忙的话,会排在当前这轮之后。"),
2515
+ h(
2516
+ "div",
2517
+ { class: "muted sched-hint" },
2518
+ resets.length
2519
+ ? `额度重置:${resets.map((w) => `${w.short === "5h" ? "5 小时" : "7 天"}额度 ${fmtWhen(w.resetsAt)}`).join(",")}。`
2520
+ : "",
2521
+ "到点由服务端发出,关掉页面也会发。",
2522
+ ),
2519
2523
  );
2520
2524
  const go = h("button", { class: "btn primary" }, edit ? "保存" : "定时发送");
2521
2525
  const close = modal({ title: edit ? "编辑定时消息" : "定时发送", body, foot: h("div", { class: "modal-foot" }, go) });
package/web/index.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
6
6
  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
7
7
  <meta name="theme-color" content="#1f1f1f" media="(prefers-color-scheme: dark)" />
8
8
  <meta name="apple-mobile-web-app-capable" content="yes" />
package/web/style.css CHANGED
@@ -2038,6 +2038,16 @@ button.ctx-item:hover {
2038
2038
  color: var(--accent);
2039
2039
  }
2040
2040
 
2041
+ .topbar .mode-slot {
2042
+ display: inline-flex;
2043
+ flex-shrink: 0;
2044
+ }
2045
+
2046
+ .topbar .top-more svg {
2047
+ width: 20px;
2048
+ height: 20px;
2049
+ }
2050
+
2041
2051
  .composer.drag {
2042
2052
  border-color: var(--accent);
2043
2053
  }
@@ -3432,14 +3442,9 @@ button.list-item:hover {
3432
3442
  font: inherit;
3433
3443
  }
3434
3444
 
3435
- .sched-presets {
3436
- display: flex;
3437
- flex-wrap: wrap;
3438
- gap: 6px;
3439
- }
3440
-
3441
- .sched-presets .btn {
3442
- font-size: 13px;
3445
+ .sched-hint {
3446
+ font-size: 12px;
3447
+ line-height: 1.6;
3443
3448
  }
3444
3449
 
3445
3450
  .sched-row {
@@ -3458,3 +3463,9 @@ button.list-item:hover {
3458
3463
  font: inherit;
3459
3464
  color-scheme: light dark;
3460
3465
  }
3466
+
3467
+ /* iOS 上双击会放大页面:关掉双击缩放(双指缩放照常) */
3468
+ html,
3469
+ body {
3470
+ touch-action: manipulation;
3471
+ }