agent-loft 0.2.30 → 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.
@@ -0,0 +1,455 @@
1
+ /* ---------- 定时任务 ---------- */
2
+
3
+ /* 侧栏「最近」上面的入口:和「新聊天」同一个样子 */
4
+ .side-nav {
5
+ display: flex;
6
+ justify-content: flex-start;
7
+ width: calc(100% - 20px);
8
+ margin: 2px 10px 6px;
9
+ padding: 7px 10px;
10
+ gap: 8px;
11
+ background: transparent;
12
+ border: none;
13
+ font-size: 14px;
14
+ font-weight: 400;
15
+ }
16
+
17
+ .side-nav svg {
18
+ width: 16px;
19
+ height: 16px;
20
+ }
21
+
22
+ .side-nav:hover,
23
+ .side-nav.active {
24
+ background: var(--bg-hover);
25
+ }
26
+
27
+ .conv-item .task-mark {
28
+ color: var(--fg-soft);
29
+ }
30
+
31
+ /* 任务页不要输入框,也不要「回到底部」 */
32
+ .main.tasks-view .composer-wrap,
33
+ .main.tasks-view .to-bottom {
34
+ display: none;
35
+ }
36
+
37
+ .tasks-page {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 16px;
41
+ padding-top: 8px;
42
+ }
43
+
44
+ .tasks-head {
45
+ display: flex;
46
+ align-items: flex-start;
47
+ gap: 16px;
48
+ }
49
+
50
+ .tasks-head .grow {
51
+ flex: 1;
52
+ min-width: 0;
53
+ }
54
+
55
+ .tasks-head h2 {
56
+ margin: 0 0 6px;
57
+ font-size: 24px;
58
+ font-weight: 600;
59
+ }
60
+
61
+ .tasks-head p {
62
+ margin: 0;
63
+ font-size: 13px;
64
+ line-height: 1.6;
65
+ color: var(--fg-soft);
66
+ }
67
+
68
+ .tasks-head .btn {
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ .task-list {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 12px;
76
+ }
77
+
78
+ .task-card {
79
+ border: 1px solid var(--border);
80
+ border-radius: var(--radius);
81
+ padding: 12px 14px 12px 16px;
82
+ background: var(--bg);
83
+ }
84
+
85
+ .task-card.off .task-title,
86
+ .task-card.off .task-meta,
87
+ .task-card.off .task-prompt {
88
+ opacity: 0.6;
89
+ }
90
+
91
+ .task-top {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ }
96
+
97
+ .task-title {
98
+ flex: 1;
99
+ min-width: 0;
100
+ padding: 0;
101
+ text-align: left;
102
+ font-size: 16px;
103
+ font-weight: 600;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .task-meta {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: 2px 0;
113
+ margin-top: 4px;
114
+ font-size: 13px;
115
+ color: var(--fg-soft);
116
+ }
117
+
118
+ /* 分隔点挂在前一项的末尾:换行时点留在行尾,不会出现在下一行开头 */
119
+ .task-meta span:not(:last-child)::after {
120
+ content: "·";
121
+ margin: 0 8px;
122
+ color: var(--fg-faint);
123
+ }
124
+
125
+ .task-prompt {
126
+ margin-top: 8px;
127
+ font-size: 13px;
128
+ line-height: 1.6;
129
+ color: var(--fg-soft);
130
+ white-space: pre-wrap;
131
+ display: -webkit-box;
132
+ -webkit-line-clamp: 2;
133
+ -webkit-box-orient: vertical;
134
+ overflow: hidden;
135
+ }
136
+
137
+ .task-run {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 8px;
141
+ width: 100%;
142
+ min-width: 0;
143
+ margin-top: 10px;
144
+ padding: 8px 10px;
145
+ border-radius: 10px;
146
+ background: var(--bg-soft);
147
+ font-size: 13px;
148
+ text-align: left;
149
+ }
150
+
151
+ button.task-run:hover {
152
+ background: var(--bg-hover);
153
+ }
154
+
155
+ .task-run.compact {
156
+ margin-top: 2px;
157
+ padding: 6px 10px;
158
+ background: transparent;
159
+ }
160
+
161
+ .task-run.muted {
162
+ color: var(--fg-faint);
163
+ }
164
+
165
+ .task-status {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 5px;
169
+ flex-shrink: 0;
170
+ padding: 1px 8px;
171
+ border-radius: 999px;
172
+ font-size: 12px;
173
+ border: 1px solid currentColor;
174
+ }
175
+
176
+ .task-status .conv-spin {
177
+ width: 10px;
178
+ height: 10px;
179
+ }
180
+
181
+ .task-status.ok {
182
+ color: var(--ok);
183
+ }
184
+
185
+ .task-status.quiet {
186
+ color: var(--fg-faint);
187
+ }
188
+
189
+ .task-status.bad {
190
+ color: var(--danger);
191
+ }
192
+
193
+ .task-status.run {
194
+ color: var(--accent);
195
+ }
196
+
197
+ .task-run-at {
198
+ flex-shrink: 0;
199
+ color: var(--fg-faint);
200
+ }
201
+
202
+ .task-run-text {
203
+ flex: 1;
204
+ min-width: 0;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ color: var(--fg-soft);
209
+ }
210
+
211
+ .task-history summary {
212
+ margin-top: 8px;
213
+ font-size: 12px;
214
+ color: var(--fg-faint);
215
+ cursor: pointer;
216
+ }
217
+
218
+ .task-history summary:hover {
219
+ color: var(--fg-soft);
220
+ }
221
+
222
+ /* 开关 */
223
+ .switch {
224
+ position: relative;
225
+ display: inline-block;
226
+ width: 36px;
227
+ height: 20px;
228
+ flex-shrink: 0;
229
+ cursor: pointer;
230
+ }
231
+
232
+ .switch input {
233
+ position: absolute;
234
+ opacity: 0;
235
+ width: 0;
236
+ height: 0;
237
+ }
238
+
239
+ .switch .slider {
240
+ position: absolute;
241
+ inset: 0;
242
+ border-radius: 999px;
243
+ background: var(--border);
244
+ transition: background 0.15s;
245
+ }
246
+
247
+ .switch .slider::before {
248
+ content: "";
249
+ position: absolute;
250
+ left: 2px;
251
+ top: 2px;
252
+ width: 16px;
253
+ height: 16px;
254
+ border-radius: 50%;
255
+ background: #fff;
256
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
257
+ transition: transform 0.15s;
258
+ }
259
+
260
+ .switch input:checked + .slider {
261
+ background: var(--accent);
262
+ }
263
+
264
+ .switch input:checked + .slider::before {
265
+ transform: translateX(16px);
266
+ }
267
+
268
+ .switch input:focus-visible + .slider {
269
+ outline: 2px solid var(--accent);
270
+ outline-offset: 2px;
271
+ }
272
+
273
+ /* 空状态 + 例子 */
274
+ .tasks-empty {
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ gap: 6px;
279
+ padding: 32px 0 8px;
280
+ text-align: center;
281
+ }
282
+
283
+ .tasks-empty-icon svg {
284
+ width: 40px;
285
+ height: 40px;
286
+ color: var(--fg-faint);
287
+ }
288
+
289
+ .tasks-empty .t {
290
+ font-size: 17px;
291
+ font-weight: 600;
292
+ }
293
+
294
+ .tasks-empty > .s {
295
+ font-size: 13px;
296
+ color: var(--fg-soft);
297
+ margin-bottom: 10px;
298
+ }
299
+
300
+ .task-examples {
301
+ display: grid;
302
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
303
+ gap: 10px;
304
+ width: 100%;
305
+ }
306
+
307
+ .task-example {
308
+ text-align: left;
309
+ padding: 12px 14px;
310
+ border: 1px solid var(--border);
311
+ border-radius: 12px;
312
+ }
313
+
314
+ .task-example:hover {
315
+ background: var(--bg-hover);
316
+ }
317
+
318
+ .task-example .t {
319
+ font-size: 14px;
320
+ font-weight: 600;
321
+ margin-bottom: 4px;
322
+ }
323
+
324
+ .task-example .s {
325
+ font-size: 12px;
326
+ line-height: 1.6;
327
+ color: var(--fg-soft);
328
+ display: -webkit-box;
329
+ -webkit-line-clamp: 3;
330
+ -webkit-box-orient: vertical;
331
+ overflow: hidden;
332
+ }
333
+
334
+ .tasks-tip {
335
+ margin: 4px 0 0;
336
+ font-size: 12px;
337
+ color: var(--fg-faint);
338
+ text-align: center;
339
+ }
340
+
341
+ /* 表单 */
342
+ .task-form [hidden] {
343
+ display: none !important;
344
+ }
345
+
346
+ .task-form textarea {
347
+ resize: vertical;
348
+ font: inherit;
349
+ line-height: 1.6;
350
+ }
351
+
352
+ .task-dedicated {
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 12px;
356
+ }
357
+
358
+ /* 频率:上面选种类,下面填具体时间 */
359
+ .task-freq-row {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 8px;
363
+ }
364
+
365
+ .task-freq {
366
+ display: flex;
367
+ flex-wrap: wrap;
368
+ align-items: center;
369
+ gap: 8px;
370
+ }
371
+
372
+ .task-freq:empty {
373
+ display: none;
374
+ }
375
+
376
+ .field .task-freq input,
377
+ .field .task-freq select {
378
+ width: auto;
379
+ color-scheme: light dark;
380
+ }
381
+
382
+ .field .task-freq input:not(.narrow):not([type="time"]) {
383
+ flex: 1;
384
+ min-width: 180px;
385
+ }
386
+
387
+ .field .task-freq .narrow {
388
+ width: 84px;
389
+ }
390
+
391
+ .freq-word {
392
+ font-size: 14px;
393
+ color: var(--fg-soft);
394
+ }
395
+
396
+ .day-chips {
397
+ display: flex;
398
+ gap: 4px;
399
+ }
400
+
401
+ .day-chip {
402
+ width: 34px;
403
+ height: 34px;
404
+ border-radius: 50%;
405
+ border: 1px solid var(--border);
406
+ font-size: 13px;
407
+ }
408
+
409
+ .day-chip.on {
410
+ background: var(--accent);
411
+ border-color: var(--accent);
412
+ color: var(--accent-fg);
413
+ }
414
+
415
+ .task-static {
416
+ font-size: 14px;
417
+ color: var(--fg-soft);
418
+ padding: 2px 0;
419
+ }
420
+
421
+ .task-cwd {
422
+ justify-content: flex-start;
423
+ overflow: hidden;
424
+ text-overflow: ellipsis;
425
+ white-space: nowrap;
426
+ }
427
+
428
+ /* 对话里:定时任务这次判断不用提醒,折叠成一行 */
429
+ .silent-run {
430
+ font-size: 13px;
431
+ color: var(--fg-faint);
432
+ }
433
+
434
+ .silent-run summary {
435
+ cursor: pointer;
436
+ list-style: none;
437
+ }
438
+
439
+ .silent-run summary::-webkit-details-marker {
440
+ display: none;
441
+ }
442
+
443
+ .silent-run summary:hover {
444
+ color: var(--fg-soft);
445
+ }
446
+
447
+ .silent-run[open] summary {
448
+ margin-bottom: 8px;
449
+ }
450
+
451
+ @media (max-width: 640px) {
452
+ .tasks-head h2 {
453
+ font-size: 21px;
454
+ }
455
+ }
package/web/index.html CHANGED
@@ -24,6 +24,7 @@
24
24
  <link rel="stylesheet" href="/css/overlay.css" />
25
25
  <link rel="stylesheet" href="/css/settings.css" />
26
26
  <link rel="stylesheet" href="/css/schedule.css" />
27
+ <link rel="stylesheet" href="/css/tasks.css" />
27
28
  <link rel="stylesheet" href="/css/mobile.css" />
28
29
  </head>
29
30
  <body>
package/web/lib/ws.js CHANGED
@@ -197,6 +197,10 @@ function onWsMessage(msg) {
197
197
  case "ix":
198
198
  upsertInteraction(msg.interaction);
199
199
  return;
200
+ case "tasks":
201
+ // 自己的定时任务变了:定时任务页开着的话会去重新拉(views/tasks.js)
202
+ document.dispatchEvent(new Event("aim:tasks"));
203
+ return;
200
204
  case "notice":
201
205
  // 正看着这条会话就不打扰;普通网页靠 Web Push,这里只给桌面端
202
206
  if (!desktop) return;
package/web/state.js CHANGED
@@ -21,6 +21,8 @@ export const S = {
21
21
  loadToken: 0,
22
22
  /** 登录的用户 { id, name, role } */
23
23
  user: null,
24
+ /** 主区域在显示什么:null 是对话(或新对话),"tasks" 是定时任务页 */
25
+ view: null,
24
26
  };
25
27
 
26
28
  export const isAdmin = () => S.user?.role === "admin";
@@ -198,12 +198,16 @@ export async function openSessions(all) {
198
198
  }
199
199
 
200
200
  /** 目录选择器:一层层点进去选,也能手动输入。只能在 workspaceRoot 之内 */
201
- export async function changeCwd() {
201
+ /**
202
+ * 选工作目录。pick 给了就是别处借用(定时任务的表单):从 pick.current 开始,选好交给 pick.onPick,
203
+ * 不动当前对话。
204
+ */
205
+ export async function changeCwd(pick) {
202
206
  // 还没发消息的新对话:只记下选择,创建会话时带过去
203
- const draft = !S.current;
204
- const conv = draft ? null : S.convs.get(S.current);
205
- const current = draft ? (S.draftCwd ?? S.workspaceRoot ?? "") : (conv?.cwd ?? "");
206
- const goLabel = draft ? "使用此目录" : "切换到此目录";
207
+ const draft = !pick && !S.current;
208
+ const conv = draft || pick ? null : S.convs.get(S.current);
209
+ const current = pick ? pick.current : draft ? (S.draftCwd ?? S.workspaceRoot ?? "") : (conv?.cwd ?? "");
210
+ const goLabel = pick || draft ? "使用此目录" : "切换到此目录";
207
211
  let showHidden = localStorage.getItem("aim.cwdHidden") === "1";
208
212
  let listing = null;
209
213
  let seq = 0;
@@ -231,9 +235,9 @@ export async function changeCwd() {
231
235
  list,
232
236
  );
233
237
  const go = h("button", { class: "btn primary", disabled: true }, goLabel);
234
- const note = h("span", { class: "muted grow" }, draft ? "发送第一条消息时在这个目录开会话" : "切换后开新会话");
238
+ const note = h("span", { class: "muted grow" }, pick ? "" : draft ? "发送第一条消息时在这个目录开会话" : "切换后开新会话");
235
239
  const close = modal({
236
- title: draft ? "选择工作目录" : "切换工作目录",
240
+ title: pick || draft ? "选择工作目录" : "切换工作目录",
237
241
  body,
238
242
  foot: h("div", { class: "modal-foot", style: { alignItems: "center" } }, note, go),
239
243
  });
@@ -241,6 +245,10 @@ export async function changeCwd() {
241
245
  go.onclick = async () => {
242
246
  if (!listing) return;
243
247
  close();
248
+ if (pick) {
249
+ pick.onPick(listing.path);
250
+ return;
251
+ }
244
252
  if (draft) {
245
253
  S.draftCwd = listing.path === listing.root ? null : listing.path;
246
254
  if (!S.current) renderEmpty();
@@ -47,6 +47,14 @@ function openSettings() {
47
47
  );
48
48
  }
49
49
 
50
+ /** 定时任务页:第一次打开时才加载 */
51
+ export function openTasksView() {
52
+ import("./tasks.js").then(
53
+ (m) => m.openTasks(),
54
+ (err) => toast(`定时任务页加载失败,刷新页面再试:${err.message}`),
55
+ );
56
+ }
57
+
50
58
  export async function boot() {
51
59
  const me = await api("/api/me");
52
60
  if (!me.authed) return showAuth();
@@ -72,7 +80,8 @@ export async function boot() {
72
80
  const fromHash = decodeURIComponent(location.hash.slice(1));
73
81
  // 地址里带了会话就打开它;否则打开「最近」里最近活跃的那个对话,一个都没有才进新对话页
74
82
  const latest = [...S.convs.values()].filter((c) => !c.hidden).sort((a, b) => b.activeAt - a.activeAt)[0];
75
- if (fromHash && S.convs.has(fromHash)) await openConversation(fromHash);
83
+ if (fromHash === "tasks") openTasksView();
84
+ else if (fromHash && S.convs.has(fromHash)) await openConversation(fromHash);
76
85
  else if (latest) await openConversation(latest.key);
77
86
  else newDraft();
78
87
  // 点了桌面端的系统通知 / 菜单栏里的待处理项:跳到对应会话
@@ -159,6 +168,8 @@ function renderLayout() {
159
168
  { class: "side-top" },
160
169
  h("button", { class: "btn new-chat", onclick: () => (newDraft(), closeSide()) }, icon("compose"), "新聊天"),
161
170
  ),
171
+ // 「最近」上面的入口,像 ChatGPT 的 Tasks。当前在任务页时高亮(renderSidebar 里切换)
172
+ (ui.tasksNav = h("button", { class: "btn new-chat side-nav", onclick: () => openTasksView() }, icon("clock"), "定时任务")),
162
173
  ui.convList,
163
174
  h(
164
175
  "div",
@@ -13,7 +13,15 @@ import { changeCwd, forkConversation } from "./conv-actions.js";
13
13
 
14
14
  // ---------- 打开会话 ----------
15
15
 
16
+ /** 从定时任务页回到对话:把输入框放回来 */
17
+ function leaveTasksView() {
18
+ if (S.view !== "tasks") return;
19
+ S.view = null;
20
+ ui.main.classList.remove("tasks-view");
21
+ }
22
+
16
23
  export function newDraft() {
24
+ leaveTasksView();
17
25
  if (S.current) WS.raw({ t: "unsub", key: S.current });
18
26
  S.current = null;
19
27
  S.items = [];
@@ -57,6 +65,7 @@ export function renderEmpty() {
57
65
  }
58
66
 
59
67
  export async function openConversation(key) {
68
+ leaveTasksView();
60
69
  const token = ++S.loadToken;
61
70
  if (S.current && S.current !== key) WS.raw({ t: "unsub", key: S.current });
62
71
  S.current = key;
@@ -349,8 +358,13 @@ function renderItem(item) {
349
358
  return h("div");
350
359
  }
351
360
 
361
+ /** 定时任务的 agent 判断这次不用提醒时,回复第一行写的暗号(见 core/task-prompt.ts) */
362
+ const SILENT_MARK = "[[静默]]";
363
+
352
364
  function renderAi(item) {
353
- const body = item.committed + (item.partial ? (item.committed ? "\n\n" : "") + item.partial : "");
365
+ const raw = item.committed + (item.partial ? (item.committed ? "\n\n" : "") + item.partial : "");
366
+ const silent = raw.trimStart().startsWith(SILENT_MARK);
367
+ const body = silent ? raw.trimStart().slice(SILENT_MARK.length).trim() : raw;
354
368
  const working = !item.done;
355
369
  const steps =
356
370
  item.steps.length > 0
@@ -380,7 +394,14 @@ function renderAi(item) {
380
394
  "div",
381
395
  { class: "msg-ai" },
382
396
  steps,
383
- body ? renderMarkdown(body) : null,
397
+ // 定时任务这次没什么要说的:折叠成一行,展开能看到它顺手记的检查结果
398
+ silent && body
399
+ ? h("details", { class: "silent-run" }, h("summary", {}, "🔕 定时任务:这次无需提醒"), renderMarkdown(body))
400
+ : silent
401
+ ? h("div", { class: "silent-run" }, "🔕 定时任务:这次无需提醒")
402
+ : body
403
+ ? renderMarkdown(body)
404
+ : null,
384
405
  // 还在回复:末尾挂上星星动画和实时用时;什么都还没出来时旁边带一句「思考中…」
385
406
  working
386
407
  ? h(
@@ -267,6 +267,7 @@ export function renderSidebar() {
267
267
  return;
268
268
  }
269
269
  sidebarStale = false;
270
+ ui.tasksNav?.classList.toggle("active", S.view === "tasks");
270
271
  const askKeys = new Set([...S.pending.values()].filter((i) => !i.settled).map((i) => i.convKey));
271
272
  const visible = [...S.convs.values()].filter((c) => !c.hidden).sort((a, b) => b.activeAt - a.activeAt);
272
273
  const cur = S.current ? S.convs.get(S.current) : null;
@@ -294,6 +295,8 @@ export function renderSidebar() {
294
295
  onclick: () => open(c.key),
295
296
  },
296
297
  orphan ? h("span", { class: "brand-icon lead fork-mark", html: ICON.fork }) : null,
298
+ // 定时任务的专用对话
299
+ c.taskId ? h("span", { class: "brand-icon lead task-mark", title: "定时任务的对话", html: ICON.clock }) : null,
297
300
  h("span", { class: "title" }, convTitle(c)),
298
301
  status([c.key]),
299
302
  );