agent-loft 0.2.29 → 0.2.31
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 +5 -0
- package/package.json +1 -1
- package/src/agents/claude/driver.ts +3 -2
- package/src/agents/claude/session.ts +11 -4
- package/src/agents/codex/session.ts +9 -3
- package/src/channels/feishu/bot.ts +8 -4
- package/src/channels/feishu/turn-stream.ts +3 -2
- package/src/channels/web/api.ts +4 -0
- package/src/channels/web/server.ts +14 -2
- package/src/channels/web/tasks-api.ts +94 -0
- package/src/core/chat-mode.ts +14 -14
- package/src/core/hub.ts +95 -16
- package/src/core/memory-mcp.ts +1 -1
- package/src/core/personal.ts +3 -1
- package/src/core/store.ts +2 -0
- package/src/core/task-prompt.ts +38 -0
- package/src/core/task-runner.ts +232 -0
- package/src/core/task-schedule.ts +250 -0
- package/src/core/tasks-bridge.ts +252 -0
- package/src/core/tasks-mcp.ts +55 -0
- package/src/core/tasks.ts +281 -0
- package/src/core/types.ts +2 -0
- package/src/main.ts +8 -0
- package/src/mcp/tasks-server.ts +206 -0
- package/web/app.js +8 -5560
- package/web/css/base.css +291 -0
- package/web/css/composer.css +506 -0
- package/web/css/loader.css +196 -0
- package/web/css/messages.css +734 -0
- package/web/css/mobile.css +100 -0
- package/web/css/overlay.css +437 -0
- package/web/css/schedule.css +93 -0
- package/web/css/settings.css +335 -0
- package/web/css/sidebar.css +593 -0
- package/web/css/tasks.css +455 -0
- package/web/css/topbar.css +205 -0
- package/web/index.html +12 -1
- package/web/lib/api.js +26 -0
- package/web/lib/dom.js +101 -0
- package/web/lib/markdown.js +52 -0
- package/web/lib/overlay.js +144 -0
- package/web/lib/push.js +70 -0
- package/web/lib/time.js +60 -0
- package/web/lib/ws.js +236 -0
- package/web/settings/agents.js +210 -0
- package/web/settings/commands.js +411 -0
- package/web/settings/common.js +42 -0
- package/web/settings/feishu.js +181 -0
- package/web/settings/general.js +184 -0
- package/web/settings/index.js +63 -0
- package/web/settings/memory.js +366 -0
- package/web/settings/notify.js +80 -0
- package/web/settings/security.js +188 -0
- package/web/settings/users.js +189 -0
- package/web/state.js +46 -0
- package/web/views/auth.js +61 -0
- package/web/views/composer.js +324 -0
- package/web/views/conv-actions.js +412 -0
- package/web/views/interaction.js +140 -0
- package/web/views/layout.js +197 -0
- package/web/views/messages.js +531 -0
- package/web/views/quota.js +145 -0
- package/web/views/schedule.js +120 -0
- package/web/views/sidebar.js +458 -0
- package/web/views/slash.js +185 -0
- package/web/views/tasks.js +460 -0
- package/web/views/topbar.js +400 -0
- package/web/views/voice.js +189 -0
- package/web/style.css +0 -3500
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { $app, fill, h, icon, toast } from "../lib/dom.js";
|
|
2
|
+
import { api } from "../lib/api.js";
|
|
3
|
+
import { desktop, desktopQuota, desktopSettings, S, ui } from "../state.js";
|
|
4
|
+
import { clearAppBadge, WS } from "../lib/ws.js";
|
|
5
|
+
import { showAuth } from "./auth.js";
|
|
6
|
+
import {
|
|
7
|
+
closeSide,
|
|
8
|
+
marquee,
|
|
9
|
+
moveProjectDir,
|
|
10
|
+
projDirs,
|
|
11
|
+
renderSidebar,
|
|
12
|
+
reportDesktop,
|
|
13
|
+
resetProjectOrder,
|
|
14
|
+
setSideWidth,
|
|
15
|
+
sideState,
|
|
16
|
+
startMarquee,
|
|
17
|
+
startSideResize,
|
|
18
|
+
stopMarquee,
|
|
19
|
+
} from "./sidebar.js";
|
|
20
|
+
import { applyDesktopQuota, loadQuota, renderQuota } from "./quota.js";
|
|
21
|
+
import { openBotConversations, openConvContextMenu, usableAgents } from "./topbar.js";
|
|
22
|
+
import { showMenu } from "../lib/overlay.js";
|
|
23
|
+
import { bindScrollWatch, newDraft, openConversation, scrollToBottom } from "./messages.js";
|
|
24
|
+
import { renderComposer } from "./composer.js";
|
|
25
|
+
import { renameConv } from "./conv-actions.js";
|
|
26
|
+
import { releaseForeignPushSub } from "../lib/push.js";
|
|
27
|
+
|
|
28
|
+
// ---------- 主界面 ----------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 设置页的代码占前端的三分之一,平时用不到:第一次打开设置时才加载。
|
|
32
|
+
* 加载期间弹窗还没出来,连点 / 桌面端重复触发都会再开一层,所以加载中的再调用直接忽略。
|
|
33
|
+
*/
|
|
34
|
+
let settingsLoading = false;
|
|
35
|
+
function openSettings() {
|
|
36
|
+
if (settingsLoading) return;
|
|
37
|
+
settingsLoading = true;
|
|
38
|
+
import("../settings/index.js").then(
|
|
39
|
+
(m) => {
|
|
40
|
+
settingsLoading = false;
|
|
41
|
+
return m.openSettings();
|
|
42
|
+
},
|
|
43
|
+
(err) => {
|
|
44
|
+
settingsLoading = false;
|
|
45
|
+
toast(`设置加载失败,刷新页面再试:${err.message}`);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 定时任务页:第一次打开时才加载 */
|
|
51
|
+
export function openTasksView() {
|
|
52
|
+
import("./tasks.js").then(
|
|
53
|
+
(m) => m.openTasks(),
|
|
54
|
+
(err) => toast(`定时任务页加载失败,刷新页面再试:${err.message}`),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function boot() {
|
|
59
|
+
const me = await api("/api/me");
|
|
60
|
+
if (!me.authed) return showAuth();
|
|
61
|
+
WS.stopped = false;
|
|
62
|
+
WS.epoch = null;
|
|
63
|
+
WS.hellos = 0;
|
|
64
|
+
const data = await api("/api/bootstrap");
|
|
65
|
+
S.user = data.user ?? me.user ?? null;
|
|
66
|
+
S.agents = data.agents;
|
|
67
|
+
S.bots = data.bots;
|
|
68
|
+
S.workspaceRoot = data.workspaceRoot;
|
|
69
|
+
S.quotaRefreshMin = data.quotaRefreshMin ?? 10;
|
|
70
|
+
S.quotaEnabled = data.quotaEnabled !== false;
|
|
71
|
+
S.convs = new Map(data.conversations.map((c) => [c.key, c]));
|
|
72
|
+
S.pending = new Map(data.pending.map((i) => [i.id, i]));
|
|
73
|
+
const enabled = usableAgents().map((a) => a.id);
|
|
74
|
+
if (!S.draftAgent || !enabled.includes(S.draftAgent)) S.draftAgent = enabled[0] ?? null;
|
|
75
|
+
renderLayout();
|
|
76
|
+
reportDesktop();
|
|
77
|
+
WS.connect();
|
|
78
|
+
if (desktopQuota) desktop.onQuota(applyDesktopQuota);
|
|
79
|
+
else void loadQuota();
|
|
80
|
+
const fromHash = decodeURIComponent(location.hash.slice(1));
|
|
81
|
+
// 地址里带了会话就打开它;否则打开「最近」里最近活跃的那个对话,一个都没有才进新对话页
|
|
82
|
+
const latest = [...S.convs.values()].filter((c) => !c.hidden).sort((a, b) => b.activeAt - a.activeAt)[0];
|
|
83
|
+
if (fromHash === "tasks") openTasksView();
|
|
84
|
+
else if (fromHash && S.convs.has(fromHash)) await openConversation(fromHash);
|
|
85
|
+
else if (latest) await openConversation(latest.key);
|
|
86
|
+
else newDraft();
|
|
87
|
+
// 点了桌面端的系统通知 / 菜单栏里的待处理项:跳到对应会话
|
|
88
|
+
desktop?.onOpen((key) => {
|
|
89
|
+
if (key && S.convs.has(key) && key !== S.current) void openConversation(key);
|
|
90
|
+
});
|
|
91
|
+
// 桌面端「更多」里点了这台服务器的设置;已经开着就不再叠一层
|
|
92
|
+
if (desktopSettings) {
|
|
93
|
+
desktop.onOpenSettings(() => {
|
|
94
|
+
if (!document.querySelector(".overlay .settings")) void openSettings();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if ("serviceWorker" in navigator) {
|
|
98
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
99
|
+
clearAppBadge();
|
|
100
|
+
void releaseForeignPushSub().catch(() => {});
|
|
101
|
+
// 点了系统通知、页面本来就开着:跳到通知对应的会话
|
|
102
|
+
navigator.serviceWorker.addEventListener("message", (e) => {
|
|
103
|
+
if (e.data?.type !== "open") return;
|
|
104
|
+
const key = decodeURIComponent(new URL(e.data.url, location.origin).hash.slice(1));
|
|
105
|
+
if (key && S.convs.has(key) && key !== S.current) void openConversation(key);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderLayout() {
|
|
111
|
+
ui.convList = h("div", { class: "conv-list" });
|
|
112
|
+
// 双击重命名。第一下单击会打开会话、侧栏整体重绘,元素已经换了,浏览器自己的 dblclick
|
|
113
|
+
// 不一定触发;所以按 data-key 自己判断「同一条、两次按下间隔很短」
|
|
114
|
+
let lastDown = { key: null, at: 0 };
|
|
115
|
+
ui.convList.addEventListener("pointerdown", (e) => {
|
|
116
|
+
if (e.button !== 0) return;
|
|
117
|
+
const key = e.target.closest?.("[data-key]")?.dataset.key;
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
if (key && key === lastDown.key && now - lastDown.at < 400) {
|
|
120
|
+
lastDown = { key: null, at: 0 };
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
renameConv(key);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
lastDown = { key, at: now };
|
|
126
|
+
});
|
|
127
|
+
ui.convList.addEventListener("contextmenu", (e) => {
|
|
128
|
+
// 飞书机器人那一行:右键看这个机器人的所有聊天
|
|
129
|
+
const bot = e.target.closest?.("[data-bot]")?.dataset.bot;
|
|
130
|
+
if (bot) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
showMenu({ x: e.clientX, y: e.clientY }, [{ label: "历史会话…", onclick: () => openBotConversations(bot) }]);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// 项目目录那一行:调整顺序(触屏上拖不动,靠这个菜单)
|
|
136
|
+
const dir = e.target.closest?.("[data-dir]")?.dataset.dir;
|
|
137
|
+
if (dir) {
|
|
138
|
+
const i = projDirs.indexOf(dir);
|
|
139
|
+
const moves = [
|
|
140
|
+
...(i > 0 ? [{ label: "置顶", onclick: () => moveProjectDir(dir, null) }, { label: "上移", onclick: () => moveProjectDir(dir, projDirs[i - 1]) }] : []),
|
|
141
|
+
...(i >= 0 && i < projDirs.length - 1 ? [{ label: "下移", onclick: () => moveProjectDir(dir, projDirs[i + 1], true) }] : []),
|
|
142
|
+
];
|
|
143
|
+
const reset = sideState.order.length ? [{ label: "恢复按最近活跃排序", onclick: resetProjectOrder }] : [];
|
|
144
|
+
const items = moves.length && reset.length ? [...moves, "sep", ...reset] : [...moves, ...reset];
|
|
145
|
+
if (!items.length) return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
showMenu({ x: e.clientX, y: e.clientY }, items);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const key = e.target.closest?.("[data-key]")?.dataset.key;
|
|
151
|
+
if (!key || !S.convs.has(key)) return;
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
openConvContextMenu(key, { x: e.clientX, y: e.clientY });
|
|
154
|
+
});
|
|
155
|
+
ui.convList.addEventListener("pointerover", (e) => {
|
|
156
|
+
const title = e.target.closest?.(".conv-item")?.querySelector(".title");
|
|
157
|
+
if (title && title !== marquee.el) startMarquee(title);
|
|
158
|
+
});
|
|
159
|
+
ui.convList.addEventListener("pointerout", (e) => {
|
|
160
|
+
const item = e.target.closest?.(".conv-item");
|
|
161
|
+
if (item && !item.contains(e.relatedTarget)) stopMarquee();
|
|
162
|
+
});
|
|
163
|
+
ui.sidebar = h(
|
|
164
|
+
"aside",
|
|
165
|
+
{ class: "sidebar" },
|
|
166
|
+
h(
|
|
167
|
+
"div",
|
|
168
|
+
{ class: "side-top" },
|
|
169
|
+
h("button", { class: "btn new-chat", onclick: () => (newDraft(), closeSide()) }, icon("compose"), "新聊天"),
|
|
170
|
+
),
|
|
171
|
+
// 「最近」上面的入口,像 ChatGPT 的 Tasks。当前在任务页时高亮(renderSidebar 里切换)
|
|
172
|
+
(ui.tasksNav = h("button", { class: "btn new-chat side-nav", onclick: () => openTasksView() }, icon("clock"), "定时任务")),
|
|
173
|
+
ui.convList,
|
|
174
|
+
h(
|
|
175
|
+
"div",
|
|
176
|
+
{ class: "side-bottom" },
|
|
177
|
+
(ui.quota = h("div", { class: "quota", onpointerenter: () => renderQuota() })),
|
|
178
|
+
h("button", { class: "icon-btn side-gear", title: "设置", onclick: () => (openSettings(), closeSide()) }, icon("gear")),
|
|
179
|
+
),
|
|
180
|
+
h("div", { class: "side-resizer", title: "拖动调整宽度,双击恢复默认", onpointerdown: startSideResize, ondblclick: () => setSideWidth(null) }),
|
|
181
|
+
);
|
|
182
|
+
ui.topbar = h("div", { class: "topbar" });
|
|
183
|
+
// 聊天 / 工作切换在顶栏右上角;先建好,renderTopbar 每次把它放回去
|
|
184
|
+
ui.modeSlot = h("span", { class: "mode-slot" });
|
|
185
|
+
ui.msgInner = h("div", { class: "messages-inner" });
|
|
186
|
+
ui.spacer = h("div", { class: "messages-spacer", "aria-hidden": "true" });
|
|
187
|
+
ui.messages = h("div", { class: "messages", tabindex: "-1" }, ui.msgInner, ui.spacer);
|
|
188
|
+
ui.toBottom = h("button", { class: "to-bottom", title: "回到底部", hidden: true, onclick: () => scrollToBottom() }, icon("down"));
|
|
189
|
+
ui.follow = true;
|
|
190
|
+
bindScrollWatch();
|
|
191
|
+
ui.composerWrap = h("div", { class: "composer-wrap" });
|
|
192
|
+
ui.main = h("main", { class: "main" }, ui.topbar, ui.messages, ui.composerWrap);
|
|
193
|
+
ui.layout = h("div", { class: "layout" }, ui.sidebar, h("div", { class: "scrim", onclick: closeSide }), ui.main);
|
|
194
|
+
fill($app, ui.layout, h("div", { class: "edge-strip", "aria-hidden": "true" }));
|
|
195
|
+
renderComposer();
|
|
196
|
+
renderSidebar();
|
|
197
|
+
}
|