agent-loft 0.2.23 → 0.2.24

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
@@ -46,7 +46,7 @@ Logo 就是这个意思:一大两小几颗星(多个 agent)站在同一张
46
46
  - 语音输入:用浏览器自带的语音识别(Chrome / Edge / Safari,需要 HTTPS)。
47
47
  - 新对话页:点大标题里的目录名,在目录树里逐层选工作目录;输入框上方选 agent,右下角选模型和思考程度。会话进行中用 `/agent` 切换 agent,模型和思考程度随时可换。
48
48
  - 输入框里的小圆环显示上下文窗口占用;模型按钮显示实际的模型和思考程度(如「Opus 5.5 high」)。
49
- - 侧栏分「最近」和「项目」两组:Web 会话按工作目录归类,飞书机器人各占一行(右键看历史会话);分叉出来的会话用虚线挂在原会话下面;会话标题由 agent 自动生成,双击重命名。
49
+ - 侧栏分「最近」和「项目」两组:Web 会话按工作目录归类(目录可以拖动排序,右键可置顶),飞书机器人各占一行(右键看历史会话);分叉出来的会话用虚线挂在原会话下面;会话标题由 agent 自动生成,双击重命名。
50
50
  - 侧栏底部显示 Claude / Codex 的订阅额度(5 小时 / 7 天窗口,以及多久后重置)。
51
51
  - 「添加到主屏幕」后当 App 用;切到后台再回来会自动重连,补上错过的内容。
52
52
  - **系统通知**:Web 会话回复完成、出错、需要授权或提问时推送到手机 / 电脑(设置 → 通知 → 「在此设备开启通知」,可以发测试通知)。飞书会话由飞书提醒,正在前台看着的会话不推。需要 HTTPS 访问,iPhone 需 iOS 16.4+ 且从主屏幕图标打开。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-loft",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "type": "module",
5
5
  "description": "多个 agent 的工作台:把本机 Claude Code / Codex 放到一处统一管理,用飞书和 Web 页面随时驱动——会话、审批、模型切换都共用",
6
6
  "keywords": [
package/web/app.js CHANGED
@@ -273,7 +273,7 @@ const desktopQuota = typeof desktop?.onQuota === "function";
273
273
  // 桌面端里:顶栏、侧栏顶部可以按住拖动窗口(样式见 style.css 的 .in-desktop)
274
274
  if (desktop) document.documentElement.classList.add("in-desktop");
275
275
  /**
276
- * 新版桌面端顶栏的「更多」里能打开每台服务器的设置,额度也在它的菜单栏里:侧栏底部的额度和设置就不显示了。
276
+ * 新版桌面端顶栏的「更多」里能打开每台服务器的设置:侧栏底部的设置按钮就不显示了(额度照常显示,数据由桌面端推)。
277
277
  * 老版本桌面端没有这个入口,照旧显示,免得设置打不开。
278
278
  */
279
279
  const desktopSettings = typeof desktop?.onOpenSettings === "function";
@@ -633,6 +633,21 @@ function renderLayout() {
633
633
  showMenu({ x: e.clientX, y: e.clientY }, [{ label: "历史会话…", onclick: () => openBotConversations(bot) }]);
634
634
  return;
635
635
  }
636
+ // 项目目录那一行:调整顺序(触屏上拖不动,靠这个菜单)
637
+ const dir = e.target.closest?.("[data-dir]")?.dataset.dir;
638
+ if (dir) {
639
+ const i = projDirs.indexOf(dir);
640
+ const moves = [
641
+ ...(i > 0 ? [{ label: "置顶", onclick: () => moveProjectDir(dir, null) }, { label: "上移", onclick: () => moveProjectDir(dir, projDirs[i - 1]) }] : []),
642
+ ...(i >= 0 && i < projDirs.length - 1 ? [{ label: "下移", onclick: () => moveProjectDir(dir, projDirs[i + 1], true) }] : []),
643
+ ];
644
+ const reset = sideState.order.length ? [{ label: "恢复按最近活跃排序", onclick: resetProjectOrder }] : [];
645
+ const items = moves.length && reset.length ? [...moves, "sep", ...reset] : [...moves, ...reset];
646
+ if (!items.length) return;
647
+ e.preventDefault();
648
+ showMenu({ x: e.clientX, y: e.clientY }, items);
649
+ return;
650
+ }
636
651
  const key = e.target.closest?.("[data-key]")?.dataset.key;
637
652
  if (!key || !S.convs.has(key)) return;
638
653
  e.preventDefault();
@@ -997,7 +1012,43 @@ function agentShort(id) {
997
1012
  }
998
1013
 
999
1014
  /** 侧栏里展开 / 收起的状态:哪些项目目录展开了、「最近」和各目录是否显示全部 */
1000
- const sideState = { open: [], more: [], folded: [], ...JSON.parse(localStorage.getItem("aim.side") || "{}") };
1015
+ const sideState = { open: [], more: [], folded: [], order: [], ...JSON.parse(localStorage.getItem("aim.side") || "{}") };
1016
+
1017
+ /**
1018
+ * 「项目」目录的顺序:没手动排过时按最近活跃;拖过之后按 sideState.order,
1019
+ * 之后新出现的目录排在已排好的后面(同样按活跃),不会挤掉用户置顶的目录
1020
+ */
1021
+ function orderProjectDirs(dirs) {
1022
+ const rank = new Map(sideState.order.map((d, i) => [d, i]));
1023
+ const ranked = dirs.filter((d) => rank.has(d)).sort((a, b) => rank.get(a) - rank.get(b));
1024
+ return [...ranked, ...dirs.filter((d) => !rank.has(d))];
1025
+ }
1026
+
1027
+ /** 当前侧栏里显示的目录顺序(renderSidebar 时记下,拖拽 / 右键菜单用) */
1028
+ let projDirs = [];
1029
+
1030
+ /** 把目录 dir 挪到 target 前面(after 为 true 时挪到后面);target 为 null 表示挪到最前 */
1031
+ function moveProjectDir(dir, target, after = false) {
1032
+ if (dir === target) return;
1033
+ const list = projDirs.filter((d) => d !== dir);
1034
+ const idx = target === null ? 0 : list.indexOf(target);
1035
+ if (idx < 0) return;
1036
+ list.splice(idx + (after ? 1 : 0), 0, dir);
1037
+ // 暂时没显示出来的目录(会话都被隐藏了之类)保留原来的名次,排在后面
1038
+ sideState.order = [...list, ...sideState.order.filter((d) => !list.includes(d))];
1039
+ localStorage.setItem("aim.side", JSON.stringify(sideState));
1040
+ renderSidebar();
1041
+ }
1042
+
1043
+ function resetProjectOrder() {
1044
+ sideState.order = [];
1045
+ localStorage.setItem("aim.side", JSON.stringify(sideState));
1046
+ renderSidebar();
1047
+ }
1048
+
1049
+ /** 正在拖的目录。拖的过程中侧栏不重绘(重绘会换掉被拖的元素,拖拽就断了),松手后补一次 */
1050
+ let dirDrag = null;
1051
+ let sidebarStale = false;
1001
1052
 
1002
1053
  /** 「最近」「项目」分组标题:点击折叠 / 展开,hover 时显示箭头 */
1003
1054
  function groupHead(label, id, folded) {
@@ -1032,6 +1083,11 @@ function reportDesktop() {
1032
1083
 
1033
1084
  function renderSidebar() {
1034
1085
  if (!ui.convList) return;
1086
+ if (dirDrag) {
1087
+ sidebarStale = true;
1088
+ return;
1089
+ }
1090
+ sidebarStale = false;
1035
1091
  const askKeys = new Set([...S.pending.values()].filter((i) => !i.settled).map((i) => i.convKey));
1036
1092
  const visible = [...S.convs.values()].filter((c) => !c.hidden).sort((a, b) => b.activeAt - a.activeAt);
1037
1093
  const cur = S.current ? S.convs.get(S.current) : null;
@@ -1115,17 +1171,68 @@ function renderSidebar() {
1115
1171
  }
1116
1172
  const projFolded = sideState.folded.includes("@projects");
1117
1173
  if (byDir.size) nodes.push(groupHead("项目", "@projects", projFolded));
1174
+ projDirs = orderProjectDirs([...byDir.keys()]);
1118
1175
  if (byDir.size && !projFolded) {
1119
- for (const [dir, convs] of byDir) {
1176
+ const clearDrop = () => ui.convList.querySelectorAll(".drop-before, .drop-after").forEach((el) => el.classList.remove("drop-before", "drop-after"));
1177
+ const endDrag = () => {
1178
+ dirDrag = null;
1179
+ clearDrop();
1180
+ if (sidebarStale) renderSidebar();
1181
+ };
1182
+ for (const dir of projDirs) {
1183
+ const convs = byDir.get(dir);
1120
1184
  const expanded = sideState.open.includes(dir);
1121
1185
  const showAll = sideState.more.includes(dir);
1186
+ // 鼠标在目标行的上半截就插到它前面,下半截插到后面
1187
+ const isAfter = (e, row) => {
1188
+ const r = row.getBoundingClientRect();
1189
+ return e.clientY > r.top + r.height / 2;
1190
+ };
1122
1191
  nodes.push(
1123
1192
  h(
1124
1193
  "div",
1125
- { class: "proj-row" },
1194
+ {
1195
+ class: "proj-row",
1196
+ "data-dir": dir,
1197
+ draggable: "true",
1198
+ ondragstart: (e) => {
1199
+ dirDrag = dir;
1200
+ e.dataTransfer.effectAllowed = "move";
1201
+ e.dataTransfer.setData("text/plain", dir);
1202
+ e.currentTarget.classList.add("dragging");
1203
+ stopMarquee();
1204
+ },
1205
+ ondragend: (e) => {
1206
+ e.currentTarget.classList.remove("dragging");
1207
+ endDrag();
1208
+ },
1209
+ ondragover: (e) => {
1210
+ if (!dirDrag) return;
1211
+ e.preventDefault();
1212
+ e.dataTransfer.dropEffect = "move";
1213
+ const after = isAfter(e, e.currentTarget);
1214
+ if (!e.currentTarget.classList.contains(after ? "drop-after" : "drop-before")) {
1215
+ clearDrop();
1216
+ if (dirDrag !== dir) e.currentTarget.classList.add(after ? "drop-after" : "drop-before");
1217
+ }
1218
+ },
1219
+ ondragleave: (e) => {
1220
+ if (!e.currentTarget.contains(e.relatedTarget)) e.currentTarget.classList.remove("drop-before", "drop-after");
1221
+ },
1222
+ ondrop: (e) => {
1223
+ if (!dirDrag) return;
1224
+ e.preventDefault();
1225
+ const from = dirDrag;
1226
+ const after = isAfter(e, e.currentTarget);
1227
+ dirDrag = null;
1228
+ clearDrop();
1229
+ if (from !== dir) moveProjectDir(from, dir, after);
1230
+ else if (sidebarStale) renderSidebar();
1231
+ },
1232
+ },
1126
1233
  h(
1127
1234
  "button",
1128
- { class: "conv-item proj", title: dir, onclick: () => toggleSide("open", dir) },
1235
+ { class: "conv-item proj", title: `${dir}\n拖动可调整顺序,右键可置顶`, onclick: () => toggleSide("open", dir) },
1129
1236
  h("span", { class: "proj-icon", html: expanded ? ICON.folderOpen : ICON.folder }),
1130
1237
  h("span", { class: "title" }, baseName(dir)),
1131
1238
  expanded ? null : status(convs.map((c) => c.key)),
package/web/style.css CHANGED
@@ -431,6 +431,32 @@ button.conv-group:hover {
431
431
  gap: 10px;
432
432
  }
433
433
 
434
+ /* 拖动目录排序:被拖的行变淡,落点处画一条横线 */
435
+ .proj-row.dragging {
436
+ opacity: 0.4;
437
+ }
438
+
439
+ .proj-row.drop-before::before,
440
+ .proj-row.drop-after::after {
441
+ content: "";
442
+ position: absolute;
443
+ left: 8px;
444
+ right: 8px;
445
+ height: 2px;
446
+ border-radius: 1px;
447
+ background: var(--accent, #3b82f6);
448
+ pointer-events: none;
449
+ z-index: 1;
450
+ }
451
+
452
+ .proj-row.drop-before::before {
453
+ top: -2px;
454
+ }
455
+
456
+ .proj-row.drop-after::after {
457
+ bottom: -2px;
458
+ }
459
+
434
460
  .proj-icon {
435
461
  display: inline-grid;
436
462
  color: var(--fg-soft);
@@ -559,8 +585,8 @@ button.conv-group:hover {
559
585
  margin-left: auto;
560
586
  }
561
587
 
562
- /* 桌面端里:额度在菜单栏、设置在顶栏「更多」里,侧栏底部整条不要 */
563
- .desktop-settings .side-bottom {
588
+ /* 桌面端里:设置在顶栏「更多」里,侧栏底部只留额度(额度由桌面端统一查好推过来) */
589
+ .desktop-settings .side-gear {
564
590
  display: none;
565
591
  }
566
592