@agent-link/server 0.1.157 → 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/package.json +1 -1
- package/web/app.js +655 -59
- package/web/modules/backgroundRouting.js +2 -1
- package/web/modules/connection.js +22 -1
- package/web/modules/loop.js +337 -0
- package/web/modules/loopTemplates.js +110 -0
- package/web/modules/team.js +8 -8
- package/web/style.css +682 -13
|
@@ -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:
|
|
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
|
+
}
|
package/web/modules/team.js
CHANGED
|
@@ -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
|
|
27
|
-
const
|
|
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
|
-
|
|
138
|
+
viewMode.value = 'chat';
|
|
139
139
|
historicalTeam.value = null;
|
|
140
140
|
activeAgentView.value = null;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function newTeam() {
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
374
|
+
teamState, viewMode, activeAgentView, historicalTeam, teamsList,
|
|
375
375
|
agentMessages,
|
|
376
376
|
// Computed
|
|
377
377
|
isTeamActive, isTeamRunning, displayTeam,
|