@agent-link/server 0.1.156 → 0.1.158

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/web/landing.html CHANGED
@@ -53,7 +53,7 @@
53
53
  --accent: #3b82f6;
54
54
  --accent-soft: rgba(59,130,246,0.15);
55
55
  --accent-glow: rgba(59,130,246,0.4);
56
- --rose: #f59e0b;
56
+ --rose: #f43f5e;
57
57
  --amber: #f59e0b;
58
58
  --cyan: #22d3ee;
59
59
  --green: #34d399;
@@ -662,7 +662,7 @@
662
662
  }
663
663
 
664
664
  .bento-icon.purple { background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(59,130,246,0.05)); color: var(--accent); }
665
- .bento-icon.rose { background: linear-gradient(135deg, rgba(245,158,11,0.2), rgba(245,158,11,0.05)); color: var(--rose); }
665
+ .bento-icon.rose { background: linear-gradient(135deg, rgba(244,63,94,0.2), rgba(244,63,94,0.05)); color: var(--rose); }
666
666
  .bento-icon.cyan { background: linear-gradient(135deg, rgba(34,211,238,0.2), rgba(34,211,238,0.05)); color: var(--cyan); }
667
667
  .bento-icon.green { background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(52,211,153,0.05)); color: var(--green); }
668
668
  .bento-icon.amber { background: linear-gradient(135deg, rgba(245,158,11,0.2), rgba(245,158,11,0.05)); color: var(--amber); }
@@ -1194,7 +1194,7 @@
1194
1194
  </div>
1195
1195
 
1196
1196
  <!-- File Preview -->
1197
- <div class="bento-card">
1197
+ <div class="bento-card wide">
1198
1198
  <div class="bento-visual">
1199
1199
  <div class="bento-icon rose">
1200
1200
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg>
@@ -55,7 +55,7 @@
55
55
  --accent: #3b82f6;
56
56
  --accent-soft: rgba(59,130,246,0.15);
57
57
  --accent-glow: rgba(59,130,246,0.4);
58
- --rose: #f59e0b;
58
+ --rose: #f43f5e;
59
59
  --amber: #f59e0b;
60
60
  --cyan: #22d3ee;
61
61
  --green: #34d399;
@@ -663,7 +663,7 @@
663
663
  }
664
664
 
665
665
  .bento-icon.purple { background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(59,130,246,0.05)); color: var(--accent); }
666
- .bento-icon.rose { background: linear-gradient(135deg, rgba(245,158,11,0.2), rgba(245,158,11,0.05)); color: var(--rose); }
666
+ .bento-icon.rose { background: linear-gradient(135deg, rgba(244,63,94,0.2), rgba(244,63,94,0.05)); color: var(--rose); }
667
667
  .bento-icon.cyan { background: linear-gradient(135deg, rgba(34,211,238,0.2), rgba(34,211,238,0.05)); color: var(--cyan); }
668
668
  .bento-icon.green { background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(52,211,153,0.05)); color: var(--green); }
669
669
  .bento-icon.amber { background: linear-gradient(135deg, rgba(245,158,11,0.2), rgba(245,158,11,0.05)); color: var(--amber); }
@@ -1193,7 +1193,7 @@
1193
1193
  </div>
1194
1194
 
1195
1195
  <!-- File Preview -->
1196
- <div class="bento-card">
1196
+ <div class="bento-card wide">
1197
1197
  <div class="bento-visual">
1198
1198
  <div class="bento-icon rose">
1199
1199
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg>
@@ -45,7 +45,8 @@ export function buildHistoryBatch(history, nextId) {
45
45
  batch.push({
46
46
  id: nextId(), role: 'tool',
47
47
  toolId: h.toolId || '', toolName: h.toolName || 'unknown',
48
- toolInput: h.toolInput || '', hasResult: true,
48
+ toolInput: h.toolInput || '', hasResult: !!h.toolOutput,
49
+ toolOutput: h.toolOutput || '',
49
50
  expanded: (h.toolName === 'Edit' || h.toolName === 'TodoWrite' || h.toolName === 'Agent'),
50
51
  timestamp: h.timestamp ? new Date(h.timestamp) : new Date(),
51
52
  });
@@ -43,6 +43,10 @@ export function createConnection(deps) {
43
43
  let team = null;
44
44
  function setTeam(t) { team = t; }
45
45
 
46
+ // Loop module — set after creation to resolve circular dependency
47
+ let loop = null;
48
+ function setLoop(l) { loop = l; }
49
+
46
50
  let ws = null;
47
51
  let sessionKey = null;
48
52
  let reconnectAttempts = 0;
@@ -230,6 +234,12 @@ export function createConnection(deps) {
230
234
  return;
231
235
  }
232
236
 
237
+ // ── Loop messages: route before normal conversation routing ──
238
+ if (loop && (msg.type?.startsWith('loop_') || msg.type === 'loops_list')) {
239
+ loop.handleLoopMessage(msg);
240
+ return;
241
+ }
242
+
233
243
  // ── Multi-session: route messages to background conversations ──
234
244
  // Messages with a conversationId that doesn't match the current foreground
235
245
  // conversation are routed to their cached background state.
@@ -268,6 +278,7 @@ export function createConnection(deps) {
268
278
  }
269
279
  sidebar.requestSessionList();
270
280
  if (team) team.requestTeamsList();
281
+ if (loop) loop.requestLoopsList();
271
282
  startPing();
272
283
  wsSend({ type: 'query_active_conversations' });
273
284
  } else {
@@ -312,6 +323,7 @@ export function createConnection(deps) {
312
323
  }
313
324
  sidebar.requestSessionList();
314
325
  if (team) team.requestTeamsList();
326
+ if (loop) loop.requestLoopsList();
315
327
  startPing();
316
328
  wsSend({ type: 'query_active_conversations' });
317
329
  } else if (msg.type === 'active_conversations') {
@@ -386,6 +398,10 @@ export function createConnection(deps) {
386
398
  if (currentConversationId && currentConversationId.value) {
387
399
  processingConversations.value[currentConversationId.value] = false;
388
400
  }
401
+ // Forward error to Loop module for inline display
402
+ if (loop && loop.loopError) {
403
+ loop.loopError.value = msg.message || '';
404
+ }
389
405
  _dequeueNext();
390
406
  } else if (msg.type === 'claude_output') {
391
407
  handleClaudeOutput(msg, scheduleHighlight);
@@ -551,8 +567,13 @@ export function createConnection(deps) {
551
567
  content: 'Working directory changed to: ' + msg.workDir,
552
568
  timestamp: new Date(),
553
569
  });
570
+ // Clear old history immediately so UI doesn't show stale data
571
+ historySessions.value = [];
572
+ if (team) team.teamsList.value = [];
573
+ if (loop) loop.loopsList.value = [];
554
574
  sidebar.requestSessionList();
555
575
  if (team) team.requestTeamsList();
576
+ if (loop) loop.requestLoopsList();
556
577
  }
557
578
  };
558
579
 
@@ -603,5 +624,5 @@ export function createConnection(deps) {
603
624
  ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
604
625
  }
605
626
 
606
- return { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap };
627
+ return { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, setLoop, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap };
607
628
  }
@@ -0,0 +1,337 @@
1
+ // ── Loop mode: state management and message routing ───────────────────────────
2
+ const { ref, computed } = Vue;
3
+
4
+ import { buildHistoryBatch } from './backgroundRouting.js';
5
+
6
+ /**
7
+ * Creates the Loop mode controller.
8
+ * @param {object} deps
9
+ * @param {Function} deps.wsSend
10
+ * @param {Function} deps.scrollToBottom
11
+ */
12
+ export function createLoop(deps) {
13
+ const { wsSend, scrollToBottom } = deps;
14
+
15
+ // ── Reactive state ──────────────────────────────────
16
+
17
+ /** @type {import('vue').Ref<Array>} All Loop definitions from agent */
18
+ const loopsList = ref([]);
19
+
20
+ /** @type {import('vue').Ref<object|null>} Loop selected for detail view */
21
+ const selectedLoop = ref(null);
22
+
23
+ /** @type {import('vue').Ref<string|null>} Execution ID selected for replay */
24
+ const selectedExecution = ref(null);
25
+
26
+ /** @type {import('vue').Ref<Array>} Execution history for selectedLoop */
27
+ const executionHistory = ref([]);
28
+
29
+ /** @type {import('vue').Ref<Array>} Messages for selectedExecution replay */
30
+ const executionMessages = ref([]);
31
+
32
+ /** @type {import('vue').Ref<object>} loopId -> LoopExecution for currently running */
33
+ const runningLoops = ref({});
34
+
35
+ /** @type {import('vue').Ref<boolean>} Loading execution list */
36
+ const loadingExecutions = ref(false);
37
+
38
+ /** @type {import('vue').Ref<boolean>} Loading single execution detail */
39
+ const loadingExecution = ref(false);
40
+
41
+ /** @type {import('vue').Ref<string|null>} Loop being edited (loopId) or null for new */
42
+ const editingLoopId = ref(null);
43
+
44
+ /** @type {import('vue').Ref<string>} Error message from last loop operation (create/update) */
45
+ const loopError = ref('');
46
+
47
+ /** @type {number} Current execution history page limit */
48
+ let execPageLimit = 20;
49
+
50
+ /** @type {import('vue').Ref<boolean>} Whether more execution history may be available */
51
+ const hasMoreExecutions = ref(false);
52
+
53
+ /** @type {import('vue').Ref<boolean>} Loading more executions via pagination */
54
+ const loadingMoreExecutions = ref(false);
55
+
56
+ // ── Computed ──────────────────────────────────────
57
+
58
+ /** Whether any Loop execution is currently running */
59
+ const hasRunningLoop = computed(() => Object.keys(runningLoops.value).length > 0);
60
+
61
+ /** Get the first running loop for notification banner */
62
+ const firstRunningLoop = computed(() => {
63
+ const entries = Object.entries(runningLoops.value);
64
+ if (entries.length === 0) return null;
65
+ const [loopId, execution] = entries[0];
66
+ const loop = loopsList.value.find(l => l.id === loopId);
67
+ return { loopId, execution, name: loop?.name || 'Unknown' };
68
+ });
69
+
70
+ // ── Loop CRUD ─────────────────────────────────────
71
+
72
+ function createNewLoop(config) {
73
+ wsSend({ type: 'create_loop', ...config });
74
+ }
75
+
76
+ function updateExistingLoop(loopId, updates) {
77
+ wsSend({ type: 'update_loop', loopId, updates });
78
+ }
79
+
80
+ function deleteExistingLoop(loopId) {
81
+ wsSend({ type: 'delete_loop', loopId });
82
+ }
83
+
84
+ function toggleLoop(loopId) {
85
+ const loop = loopsList.value.find(l => l.id === loopId);
86
+ if (!loop) return;
87
+ wsSend({ type: 'update_loop', loopId, updates: { enabled: !loop.enabled } });
88
+ }
89
+
90
+ function runNow(loopId) {
91
+ wsSend({ type: 'run_loop', loopId });
92
+ }
93
+
94
+ function cancelExecution(loopId) {
95
+ wsSend({ type: 'cancel_loop_execution', loopId });
96
+ }
97
+
98
+ function requestLoopsList() {
99
+ wsSend({ type: 'list_loops' });
100
+ }
101
+
102
+ // ── Navigation ────────────────────────────────────
103
+
104
+ function viewLoopDetail(loopId) {
105
+ const loop = loopsList.value.find(l => l.id === loopId);
106
+ if (!loop) return;
107
+ selectedLoop.value = { ...loop };
108
+ selectedExecution.value = null;
109
+ executionMessages.value = [];
110
+ executionHistory.value = [];
111
+ loadingExecutions.value = true;
112
+ editingLoopId.value = null;
113
+ execPageLimit = 20;
114
+ hasMoreExecutions.value = false;
115
+ wsSend({ type: 'list_loop_executions', loopId, limit: execPageLimit });
116
+ }
117
+
118
+ function viewExecution(loopId, executionId) {
119
+ selectedExecution.value = executionId;
120
+ loadingExecution.value = true;
121
+ executionMessages.value = [];
122
+ wsSend({ type: 'get_loop_execution_messages', loopId, executionId });
123
+ }
124
+
125
+ function backToLoopsList() {
126
+ selectedLoop.value = null;
127
+ selectedExecution.value = null;
128
+ executionHistory.value = [];
129
+ executionMessages.value = [];
130
+ editingLoopId.value = null;
131
+ }
132
+
133
+ function backToLoopDetail() {
134
+ selectedExecution.value = null;
135
+ executionMessages.value = [];
136
+ }
137
+
138
+ function startEditing(loopId) {
139
+ editingLoopId.value = loopId;
140
+ }
141
+
142
+ function cancelEditing() {
143
+ editingLoopId.value = null;
144
+ }
145
+
146
+ function loadMoreExecutions() {
147
+ if (!selectedLoop.value || loadingMoreExecutions.value) return;
148
+ loadingMoreExecutions.value = true;
149
+ execPageLimit *= 2;
150
+ wsSend({ type: 'list_loop_executions', loopId: selectedLoop.value.id, limit: execPageLimit });
151
+ }
152
+
153
+ function clearLoopError() {
154
+ loopError.value = '';
155
+ }
156
+
157
+ // ── Live output accumulation ─────────────────────
158
+
159
+ /** Message ID counter for live execution messages */
160
+ let liveMsgIdCounter = 0;
161
+
162
+ /**
163
+ * Append a Claude output message to the live execution display.
164
+ * Mirrors the team.js handleTeamAgentOutput accumulation logic.
165
+ */
166
+ function appendOutputToDisplay(data) {
167
+ if (!data) return;
168
+ const msgs = executionMessages.value;
169
+
170
+ if (data.type === 'content_block_delta' && data.delta) {
171
+ const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
172
+ if (last && last.role === 'assistant' && last.isStreaming) {
173
+ last.content += data.delta;
174
+ } else {
175
+ msgs.push({
176
+ id: ++liveMsgIdCounter, role: 'assistant',
177
+ content: data.delta, isStreaming: true, timestamp: Date.now(),
178
+ });
179
+ }
180
+ } else if (data.type === 'tool_use' && data.tools) {
181
+ const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
182
+ if (last && last.role === 'assistant' && last.isStreaming) {
183
+ last.isStreaming = false;
184
+ }
185
+ for (const tool of data.tools) {
186
+ msgs.push({
187
+ id: ++liveMsgIdCounter, role: 'tool',
188
+ toolId: tool.id, toolName: tool.name || 'unknown',
189
+ toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
190
+ hasResult: false, expanded: true, timestamp: Date.now(),
191
+ });
192
+ }
193
+ } else if (data.type === 'user' && data.tool_use_result) {
194
+ const result = data.tool_use_result;
195
+ const results = Array.isArray(result) ? result : [result];
196
+ for (const r of results) {
197
+ const toolMsg = msgs.find(m => m.role === 'tool' && m.toolId === r.tool_use_id);
198
+ if (toolMsg) {
199
+ toolMsg.toolOutput = typeof r.content === 'string'
200
+ ? r.content : JSON.stringify(r.content, null, 2);
201
+ toolMsg.hasResult = true;
202
+ }
203
+ }
204
+ }
205
+
206
+ scrollToBottom();
207
+ }
208
+
209
+ // ── Message routing ───────────────────────────────
210
+
211
+ /**
212
+ * Handle incoming Loop-related messages from the WebSocket.
213
+ * Returns true if the message was consumed.
214
+ */
215
+ function handleLoopMessage(msg) {
216
+ switch (msg.type) {
217
+ case 'loops_list':
218
+ loopsList.value = msg.loops || [];
219
+ return true;
220
+
221
+ case 'loop_created':
222
+ loopsList.value.push(msg.loop);
223
+ loopError.value = '';
224
+ return true;
225
+
226
+ case 'loop_updated': {
227
+ const idx = loopsList.value.findIndex(l => l.id === msg.loop.id);
228
+ if (idx >= 0) loopsList.value[idx] = msg.loop;
229
+ if (selectedLoop.value?.id === msg.loop.id) {
230
+ selectedLoop.value = { ...msg.loop };
231
+ }
232
+ editingLoopId.value = null;
233
+ loopError.value = '';
234
+ return true;
235
+ }
236
+
237
+ case 'loop_deleted':
238
+ loopsList.value = loopsList.value.filter(l => l.id !== msg.loopId);
239
+ if (selectedLoop.value?.id === msg.loopId) backToLoopsList();
240
+ return true;
241
+
242
+ case 'loop_execution_started':
243
+ runningLoops.value = { ...runningLoops.value, [msg.loopId]: msg.execution };
244
+ // If viewing this loop's detail, prepend to history
245
+ if (selectedLoop.value?.id === msg.loopId) {
246
+ executionHistory.value.unshift(msg.execution);
247
+ }
248
+ return true;
249
+
250
+ case 'loop_execution_output':
251
+ // If user is viewing this execution live, append to display
252
+ if (selectedExecution.value === msg.executionId) {
253
+ appendOutputToDisplay(msg.data);
254
+ }
255
+ return true;
256
+
257
+ case 'loop_execution_completed': {
258
+ const newRunning = { ...runningLoops.value };
259
+ delete newRunning[msg.loopId];
260
+ runningLoops.value = newRunning;
261
+ // Update execution in history list
262
+ if (selectedLoop.value?.id === msg.loopId) {
263
+ const idx = executionHistory.value.findIndex(e => e.id === msg.execution.id);
264
+ if (idx >= 0) executionHistory.value[idx] = msg.execution;
265
+ }
266
+ // Finalize streaming message
267
+ const msgs = executionMessages.value;
268
+ if (msgs.length > 0) {
269
+ const last = msgs[msgs.length - 1];
270
+ if (last.role === 'assistant' && last.isStreaming) {
271
+ last.isStreaming = false;
272
+ }
273
+ }
274
+ // Update Loop's lastExecution in sidebar list
275
+ const loop = loopsList.value.find(l => l.id === msg.loopId);
276
+ if (loop) {
277
+ loop.lastExecution = {
278
+ id: msg.execution.id,
279
+ status: msg.execution.status,
280
+ startedAt: msg.execution.startedAt,
281
+ durationMs: msg.execution.durationMs,
282
+ trigger: msg.execution.trigger,
283
+ };
284
+ }
285
+ return true;
286
+ }
287
+
288
+ case 'loop_executions_list':
289
+ if (selectedLoop.value?.id === msg.loopId) {
290
+ const execs = msg.executions || [];
291
+ executionHistory.value = execs;
292
+ loadingExecutions.value = false;
293
+ loadingMoreExecutions.value = false;
294
+ hasMoreExecutions.value = execs.length >= execPageLimit;
295
+ }
296
+ return true;
297
+
298
+ case 'loop_execution_messages':
299
+ if (selectedExecution.value === msg.executionId) {
300
+ if (msg.messages && msg.messages.length > 0) {
301
+ let idCounter = 0;
302
+ executionMessages.value = buildHistoryBatch(msg.messages, () => ++idCounter);
303
+ liveMsgIdCounter = idCounter;
304
+ } else {
305
+ executionMessages.value = [];
306
+ }
307
+ loadingExecution.value = false;
308
+ scrollToBottom();
309
+ }
310
+ return true;
311
+
312
+ default:
313
+ return false;
314
+ }
315
+ }
316
+
317
+ return {
318
+ // State
319
+ loopsList, selectedLoop, selectedExecution,
320
+ executionHistory, executionMessages, runningLoops,
321
+ loadingExecutions, loadingExecution, editingLoopId,
322
+ loopError, hasMoreExecutions, loadingMoreExecutions,
323
+ // Computed
324
+ hasRunningLoop, firstRunningLoop,
325
+ // CRUD
326
+ createNewLoop, updateExistingLoop, deleteExistingLoop,
327
+ toggleLoop, runNow, cancelExecution, requestLoopsList,
328
+ // Navigation
329
+ viewLoopDetail, viewExecution,
330
+ backToLoopsList, backToLoopDetail,
331
+ startEditing, cancelEditing,
332
+ // Pagination & errors
333
+ loadMoreExecutions, clearLoopError,
334
+ // Message routing
335
+ handleLoopMessage,
336
+ };
337
+ }
@@ -0,0 +1,110 @@
1
+ // ── Loop template definitions ─────────────────────────────────────────────────
2
+ // Predefined sample cases for the Loop creation panel ("Try it" cards).
3
+ // Each template pre-fills name, prompt, scheduleType, and scheduleConfig.
4
+
5
+ export const LOOP_TEMPLATES = {
6
+ 'competitive-intel': {
7
+ label: 'Competitive Intel Monitor',
8
+ description: 'Track competitor products, pricing, and industry trends',
9
+ name: 'Competitive Intelligence Monitor',
10
+ prompt: `Monitor competitor and industry developments. Scan the working directory for any tracked competitor data, news feeds, or intelligence files.
11
+
12
+ 1. Identify new product launches, feature updates, or pricing changes from competitors
13
+ 2. Summarize key industry trends, regulatory changes, or market shifts
14
+ 3. Highlight strategic threats (competitors gaining ground) and opportunities (gaps in market)
15
+ 4. Compare against our current positioning where relevant
16
+
17
+ Provide a structured briefing with sections: Key Developments, Threats, Opportunities, Recommended Actions.`,
18
+ scheduleType: 'daily',
19
+ scheduleConfig: { hour: 8, minute: 0 },
20
+ },
21
+
22
+ 'knowledge-base': {
23
+ label: 'Knowledge Base Maintenance',
24
+ description: 'Audit notes and docs for broken links, orphan files, and organization',
25
+ name: 'Knowledge Base Maintenance',
26
+ prompt: `Perform a maintenance audit on the knowledge base / notes in this directory.
27
+
28
+ 1. Find broken internal links (references to files or headings that no longer exist)
29
+ 2. Identify orphan files (documents with no inbound links from any other document)
30
+ 3. Detect duplicate or near-duplicate content across files
31
+ 4. Check for outdated information (files not modified in 90+ days that reference time-sensitive topics)
32
+ 5. Suggest tag/folder reorganization for better discoverability
33
+
34
+ Provide a structured report with sections: Broken Links, Orphan Files, Duplicates, Stale Content, Reorganization Suggestions.`,
35
+ scheduleType: 'weekly',
36
+ scheduleConfig: { hour: 20, minute: 0, dayOfWeek: 5 }, // Friday 20:00
37
+ },
38
+
39
+ 'daily-summary': {
40
+ label: '日报/周报生成',
41
+ description: '根据 git log 自动总结代码变更和工作进展',
42
+ name: '每日工作总结',
43
+ prompt: `根据当前工作目录的 git log 生成今日工作总结。
44
+
45
+ 1. 列出今天所有 commit,按功能模块分组
46
+ 2. 总结主要完成的功能、修复的 bug、重构的代码
47
+ 3. 统计变更的文件数量和代码行数(新增/删除)
48
+ 4. 标注仍在进行中的工作(未完成的分支、TODO 等)
49
+ 5. 列出明日待办事项建议
50
+
51
+ 输出格式:结构化的日报,包含:今日完成、进行中、明日计划。`,
52
+ scheduleType: 'daily',
53
+ scheduleConfig: { hour: 18, minute: 0 },
54
+ },
55
+ };
56
+
57
+ export const LOOP_TEMPLATE_KEYS = ['competitive-intel', 'knowledge-base', 'daily-summary'];
58
+
59
+ /**
60
+ * Convert scheduleType + scheduleConfig into a cron expression string.
61
+ * @param {string} scheduleType - 'hourly' | 'daily' | 'weekly' | 'cron'
62
+ * @param {object} scheduleConfig - { hour?, minute?, dayOfWeek?, cronExpression? }
63
+ * @returns {string} cron expression
64
+ */
65
+ export function buildCronExpression(scheduleType, scheduleConfig) {
66
+ const min = scheduleConfig.minute ?? 0;
67
+ const hr = scheduleConfig.hour ?? 9;
68
+ switch (scheduleType) {
69
+ case 'manual':
70
+ return '';
71
+ case 'hourly':
72
+ return `${min} * * * *`;
73
+ case 'daily':
74
+ return `${min} ${hr} * * *`;
75
+ case 'weekly':
76
+ return `${min} ${hr} * * ${scheduleConfig.dayOfWeek ?? 1}`;
77
+ case 'cron':
78
+ return scheduleConfig.cronExpression || `${min} ${hr} * * *`;
79
+ default:
80
+ return `${min} ${hr} * * *`;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Format a cron expression into a human-readable description.
86
+ * @param {string} scheduleType - 'hourly' | 'daily' | 'weekly' | 'cron'
87
+ * @param {object} scheduleConfig - { hour?, minute?, dayOfWeek? }
88
+ * @param {string} cronExpr - raw cron expression (for 'cron' type)
89
+ * @returns {string}
90
+ */
91
+ export function formatSchedule(scheduleType, scheduleConfig, cronExpr) {
92
+ const pad = n => String(n).padStart(2, '0');
93
+ const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
94
+ switch (scheduleType) {
95
+ case 'manual':
96
+ return 'Manual only';
97
+ case 'hourly':
98
+ return 'Every hour';
99
+ case 'daily':
100
+ return `Every day at ${pad(scheduleConfig.hour ?? 9)}:${pad(scheduleConfig.minute ?? 0)}`;
101
+ case 'weekly': {
102
+ const day = DAYS[scheduleConfig.dayOfWeek ?? 1] || 'Monday';
103
+ return `Every ${day} at ${pad(scheduleConfig.hour ?? 9)}:${pad(scheduleConfig.minute ?? 0)}`;
104
+ }
105
+ case 'cron':
106
+ return cronExpr || 'Custom cron';
107
+ default:
108
+ return cronExpr || 'Unknown schedule';
109
+ }
110
+ }
@@ -23,8 +23,8 @@ export function createTeam(deps) {
23
23
  /** @type {import('vue').Ref<object|null>} Current team state (TeamStateSerialized or null) */
24
24
  const teamState = ref(null);
25
25
 
26
- /** @type {import('vue').Ref<string>} 'chat' | 'team' — current input mode */
27
- const teamMode = ref('chat');
26
+ /** @type {import('vue').Ref<string>} 'chat' | 'team' | 'loop' — current view mode */
27
+ const viewMode = ref('chat');
28
28
 
29
29
  /** @type {import('vue').Ref<string|null>} Currently viewed agent ID, null = dashboard */
30
30
  const activeAgentView = ref(null);
@@ -135,13 +135,13 @@ export function createTeam(deps) {
135
135
  }
136
136
 
137
137
  function backToChat() {
138
- teamMode.value = 'chat';
138
+ viewMode.value = 'chat';
139
139
  historicalTeam.value = null;
140
140
  activeAgentView.value = null;
141
141
  }
142
142
 
143
143
  function newTeam() {
144
- teamMode.value = 'team';
144
+ viewMode.value = 'team';
145
145
  historicalTeam.value = null;
146
146
  activeAgentView.value = null;
147
147
  // If completed team is still in teamState, clear it so create panel shows
@@ -161,7 +161,7 @@ export function createTeam(deps) {
161
161
  switch (msg.type) {
162
162
  case 'team_created':
163
163
  teamState.value = msg.team;
164
- teamMode.value = 'team';
164
+ viewMode.value = 'team';
165
165
  historicalTeam.value = null;
166
166
  activeAgentView.value = null;
167
167
  agentMessages.value = {};
@@ -266,7 +266,7 @@ export function createTeam(deps) {
266
266
 
267
267
  case 'team_detail':
268
268
  historicalTeam.value = msg.team;
269
- teamMode.value = 'team';
269
+ viewMode.value = 'team';
270
270
  activeAgentView.value = null;
271
271
  return true;
272
272
 
@@ -355,7 +355,7 @@ export function createTeam(deps) {
355
355
  function handleActiveTeamRestore(activeTeam) {
356
356
  if (!activeTeam) return;
357
357
  teamState.value = activeTeam;
358
- teamMode.value = 'team';
358
+ viewMode.value = 'team';
359
359
  // Re-initialize agent message lists (messages lost on reconnect)
360
360
  if (!agentMessages.value['lead']) {
361
361
  agentMessages.value['lead'] = [];
@@ -371,7 +371,7 @@ export function createTeam(deps) {
371
371
 
372
372
  return {
373
373
  // State
374
- teamState, teamMode, activeAgentView, historicalTeam, teamsList,
374
+ teamState, viewMode, activeAgentView, historicalTeam, teamsList,
375
375
  agentMessages,
376
376
  // Computed
377
377
  isTeamActive, isTeamRunning, displayTeam,