@agent-link/server 0.1.187 → 0.1.188

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.
Files changed (54) hide show
  1. package/dist/index.js +13 -15
  2. package/dist/index.js.map +1 -1
  3. package/package.json +3 -3
  4. package/web/dist/assets/index-C9bIrYkZ.js +320 -0
  5. package/web/dist/assets/index-C9bIrYkZ.js.map +1 -0
  6. package/web/dist/assets/index-Y1FN_mFe.css +1 -0
  7. package/web/{index.html → dist/index.html} +2 -19
  8. package/web/app.js +0 -2881
  9. package/web/css/ask-question.css +0 -333
  10. package/web/css/base.css +0 -270
  11. package/web/css/btw.css +0 -148
  12. package/web/css/chat.css +0 -176
  13. package/web/css/file-browser.css +0 -499
  14. package/web/css/input.css +0 -671
  15. package/web/css/loop.css +0 -674
  16. package/web/css/markdown.css +0 -169
  17. package/web/css/responsive.css +0 -314
  18. package/web/css/sidebar.css +0 -593
  19. package/web/css/team.css +0 -1277
  20. package/web/css/tools.css +0 -327
  21. package/web/encryption.js +0 -56
  22. package/web/modules/appHelpers.js +0 -100
  23. package/web/modules/askQuestion.js +0 -63
  24. package/web/modules/backgroundRouting.js +0 -269
  25. package/web/modules/connection.js +0 -731
  26. package/web/modules/fileAttachments.js +0 -125
  27. package/web/modules/fileBrowser.js +0 -398
  28. package/web/modules/filePreview.js +0 -213
  29. package/web/modules/i18n.js +0 -101
  30. package/web/modules/loop.js +0 -338
  31. package/web/modules/loopTemplates.js +0 -110
  32. package/web/modules/markdown.js +0 -83
  33. package/web/modules/messageHelpers.js +0 -206
  34. package/web/modules/sidebar.js +0 -402
  35. package/web/modules/streaming.js +0 -116
  36. package/web/modules/team.js +0 -396
  37. package/web/modules/teamTemplates.js +0 -360
  38. package/web/vendor/highlight.min.js +0 -1213
  39. package/web/vendor/marked.min.js +0 -6
  40. package/web/vendor/nacl-fast.min.js +0 -1
  41. package/web/vendor/nacl-util.min.js +0 -1
  42. package/web/vendor/pako.min.js +0 -2
  43. package/web/vendor/vue.global.prod.js +0 -13
  44. /package/web/{favicon.svg → dist/favicon.svg} +0 -0
  45. /package/web/{images → dist/images}/chat-iPad.webp +0 -0
  46. /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
  47. /package/web/{images → dist/images}/loop-iPad.webp +0 -0
  48. /package/web/{images → dist/images}/team-iPad.webp +0 -0
  49. /package/web/{landing.html → dist/landing.html} +0 -0
  50. /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
  51. /package/web/{locales → dist/locales}/en.json +0 -0
  52. /package/web/{locales → dist/locales}/zh.json +0 -0
  53. /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
  54. /package/web/{vendor → dist/vendor}/github.min.css +0 -0
@@ -1,213 +0,0 @@
1
- // ── File Preview: panel state, content rendering, resize handle ────────
2
-
3
- /**
4
- * Creates the file preview controller.
5
- * @param {object} deps - Reactive state and callbacks
6
- */
7
- export function createFilePreview(deps) {
8
- const {
9
- wsSend,
10
- previewPanelOpen,
11
- previewPanelWidth,
12
- previewFile,
13
- previewLoading,
14
- previewMarkdownRendered,
15
- sidebarView,
16
- sidebarOpen,
17
- isMobile,
18
- renderMarkdown,
19
- } = deps;
20
-
21
- // ── Open / Close ──
22
-
23
- function openPreview(filePath) {
24
- // Skip re-fetch if same file already loaded
25
- if (previewFile.value && previewFile.value.filePath === filePath && !previewFile.value.error) {
26
- if (isMobile.value) {
27
- sidebarView.value = 'preview';
28
- sidebarOpen.value = true;
29
- } else {
30
- previewPanelOpen.value = true;
31
- }
32
- return;
33
- }
34
- if (isMobile.value) {
35
- sidebarView.value = 'preview';
36
- sidebarOpen.value = true;
37
- } else {
38
- previewPanelOpen.value = true;
39
- }
40
- previewLoading.value = true;
41
- previewFile.value = null;
42
- wsSend({ type: 'read_file', filePath });
43
- }
44
-
45
- function closePreview() {
46
- if (isMobile.value) {
47
- sidebarView.value = 'files';
48
- } else {
49
- previewPanelOpen.value = false;
50
- }
51
- }
52
-
53
- // ── Handle file_content response ──
54
-
55
- function handleFileContent(msg) {
56
- previewLoading.value = false;
57
- previewMarkdownRendered.value = false;
58
- previewFile.value = {
59
- filePath: msg.filePath,
60
- fileName: msg.fileName,
61
- content: msg.content,
62
- encoding: msg.encoding,
63
- mimeType: msg.mimeType,
64
- truncated: msg.truncated,
65
- totalSize: msg.totalSize,
66
- error: msg.error || null,
67
- };
68
- }
69
-
70
- // ── Workdir changed → close preview ──
71
-
72
- function onWorkdirChanged() {
73
- previewPanelOpen.value = false;
74
- previewFile.value = null;
75
- previewLoading.value = false;
76
- if (sidebarView.value === 'preview') {
77
- sidebarView.value = 'sessions';
78
- }
79
- }
80
-
81
- // ── Syntax highlighting helpers ──
82
-
83
- const LANG_MAP = {
84
- ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
85
- mjs: 'javascript', cjs: 'javascript', py: 'python', rb: 'ruby',
86
- rs: 'rust', go: 'go', java: 'java', c: 'c', h: 'c',
87
- cpp: 'cpp', hpp: 'cpp', cs: 'csharp', swift: 'swift', kt: 'kotlin',
88
- lua: 'lua', r: 'r', sql: 'sql', sh: 'bash', bash: 'bash', zsh: 'bash',
89
- fish: 'bash', ps1: 'powershell', bat: 'dos', cmd: 'dos',
90
- json: 'json', json5: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini',
91
- xml: 'xml', html: 'xml', htm: 'xml', css: 'css', scss: 'scss', less: 'less',
92
- md: 'markdown', txt: 'plaintext', log: 'plaintext', graphql: 'graphql',
93
- proto: 'protobuf', vue: 'xml', svelte: 'xml', ini: 'ini', cfg: 'ini',
94
- conf: 'ini', env: 'bash',
95
- };
96
-
97
- function detectLanguage(fileName) {
98
- const ext = (fileName || '').split('.').pop()?.toLowerCase();
99
- return LANG_MAP[ext] || ext || 'plaintext';
100
- }
101
-
102
- function highlightCode(code, fileName) {
103
- if (!code) return '';
104
- if (!window.hljs) return escapeHtml(code);
105
- const lang = detectLanguage(fileName);
106
- try {
107
- return window.hljs.highlight(code, { language: lang }).value;
108
- } catch {
109
- try {
110
- return window.hljs.highlightAuto(code).value;
111
- } catch {
112
- return escapeHtml(code);
113
- }
114
- }
115
- }
116
-
117
- function escapeHtml(str) {
118
- return str
119
- .replace(/&/g, '&')
120
- .replace(/</g, '&lt;')
121
- .replace(/>/g, '&gt;')
122
- .replace(/"/g, '&quot;');
123
- }
124
-
125
- // ── File size formatting ──
126
-
127
- function formatFileSize(bytes) {
128
- if (bytes == null) return '';
129
- if (bytes < 1024) return bytes + ' B';
130
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
131
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
132
- }
133
-
134
- // ── Resize handle (mouse + touch) ──
135
-
136
- let _resizing = false;
137
- let _startX = 0;
138
- let _startWidth = 0;
139
- const MIN_WIDTH = 200;
140
- const MAX_WIDTH = 800;
141
-
142
- function onResizeStart(e) {
143
- e.preventDefault();
144
- _resizing = true;
145
- _startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
146
- _startWidth = previewPanelWidth.value;
147
- document.body.style.cursor = 'col-resize';
148
- document.body.style.userSelect = 'none';
149
- if (e.type === 'touchstart') {
150
- document.addEventListener('touchmove', onResizeMove, { passive: false });
151
- document.addEventListener('touchend', onResizeEnd);
152
- } else {
153
- document.addEventListener('mousemove', onResizeMove);
154
- document.addEventListener('mouseup', onResizeEnd);
155
- }
156
- }
157
-
158
- function onResizeMove(e) {
159
- if (!_resizing) return;
160
- if (e.type === 'touchmove') e.preventDefault();
161
- const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
162
- // Left edge resize: dragging left = wider, dragging right = narrower
163
- const delta = _startX - clientX;
164
- const newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, _startWidth + delta));
165
- previewPanelWidth.value = newWidth;
166
- }
167
-
168
- function onResizeEnd() {
169
- if (!_resizing) return;
170
- _resizing = false;
171
- document.body.style.cursor = '';
172
- document.body.style.userSelect = '';
173
- document.removeEventListener('mousemove', onResizeMove);
174
- document.removeEventListener('mouseup', onResizeEnd);
175
- document.removeEventListener('touchmove', onResizeMove);
176
- document.removeEventListener('touchend', onResizeEnd);
177
- localStorage.setItem('agentlink-preview-panel-width', String(previewPanelWidth.value));
178
- }
179
-
180
- // ── Markdown preview ──
181
-
182
- function isMarkdownFile(fileName) {
183
- const ext = (fileName || '').split('.').pop()?.toLowerCase();
184
- return ext === 'md' || ext === 'mdx';
185
- }
186
-
187
- function renderedMarkdownHtml(content) {
188
- return renderMarkdown(content || '');
189
- }
190
-
191
- /** Force re-fetch the currently open preview file (e.g. after editing) */
192
- function refreshPreview() {
193
- if (!previewFile.value?.filePath) return;
194
- const filePath = previewFile.value.filePath;
195
- previewFile.value = null;
196
- previewLoading.value = true;
197
- wsSend({ type: 'read_file', filePath });
198
- }
199
-
200
- return {
201
- openPreview,
202
- closePreview,
203
- refreshPreview,
204
- handleFileContent,
205
- onWorkdirChanged,
206
- detectLanguage,
207
- highlightCode,
208
- formatFileSize,
209
- onResizeStart,
210
- isMarkdownFile,
211
- renderedMarkdownHtml,
212
- };
213
- }
@@ -1,101 +0,0 @@
1
- // ── Lightweight i18n module ─────────────────────────────────────────────────
2
- const { ref, computed } = Vue;
3
-
4
- /**
5
- * Creates i18n functionality: t() translator, locale switching, persistence.
6
- * Locale data is loaded dynamically from /locales/<lang>.json.
7
- *
8
- * @returns {{ t: Function, locale: import('vue').Ref<string>, setLocale: Function }}
9
- */
10
- export function createI18n() {
11
- const STORAGE_KEY = 'agentlink-language';
12
- const SUPPORTED = ['en', 'zh'];
13
- const DEFAULT_LOCALE = 'en';
14
-
15
- // Detect initial locale
16
- function detectLocale() {
17
- // 1. Explicit user choice
18
- const stored = localStorage.getItem(STORAGE_KEY);
19
- if (stored && SUPPORTED.includes(stored)) return stored;
20
-
21
- // 2. Browser preference
22
- const nav = (navigator.language || '').toLowerCase();
23
- if (nav.startsWith('zh')) return 'zh';
24
-
25
- // 3. Default
26
- return DEFAULT_LOCALE;
27
- }
28
-
29
- const locale = ref(detectLocale());
30
- const _messages = ref({});
31
- let _loadedLocale = null;
32
-
33
- // Load locale JSON
34
- async function loadMessages(lang) {
35
- if (_loadedLocale === lang && Object.keys(_messages.value).length > 0) return;
36
- try {
37
- const resp = await fetch(`/locales/${lang}.json`);
38
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
39
- _messages.value = await resp.json();
40
- _loadedLocale = lang;
41
- } catch (e) {
42
- console.warn(`[i18n] Failed to load locale "${lang}":`, e);
43
- // Fallback: try loading English
44
- if (lang !== DEFAULT_LOCALE) {
45
- try {
46
- const resp = await fetch(`/locales/${DEFAULT_LOCALE}.json`);
47
- if (resp.ok) {
48
- _messages.value = await resp.json();
49
- _loadedLocale = DEFAULT_LOCALE;
50
- }
51
- } catch { /* give up */ }
52
- }
53
- }
54
- }
55
-
56
- /**
57
- * Translate a key, with optional parameter substitution.
58
- * Returns the key itself if no translation is found (fallback).
59
- *
60
- * @param {string} key - Dot-notation key, e.g. "button.send"
61
- * @param {object} [params] - Substitution params, e.g. { n: 5 }
62
- * @returns {string}
63
- */
64
- function t(key, params) {
65
- let str = _messages.value[key];
66
- if (str === undefined) return key;
67
- if (params) {
68
- for (const [k, v] of Object.entries(params)) {
69
- str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
70
- }
71
- }
72
- return str;
73
- }
74
-
75
- /**
76
- * Switch locale, persist choice, and reload strings.
77
- * @param {string} lang
78
- */
79
- async function setLocale(lang) {
80
- if (!SUPPORTED.includes(lang)) return;
81
- locale.value = lang;
82
- localStorage.setItem(STORAGE_KEY, lang);
83
- await loadMessages(lang);
84
- }
85
-
86
- /**
87
- * Toggle between supported locales (EN ↔ 中).
88
- */
89
- async function toggleLocale() {
90
- const next = locale.value === 'en' ? 'zh' : 'en';
91
- await setLocale(next);
92
- }
93
-
94
- // The display label for the language switcher button
95
- const localeLabel = computed(() => locale.value === 'en' ? 'EN' : '中');
96
-
97
- // Load initial messages
98
- loadMessages(locale.value);
99
-
100
- return { t, locale, setLocale, toggleLocale, localeLabel };
101
- }
@@ -1,338 +0,0 @@
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, loadingLoops } = 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
- if (loadingLoops) loadingLoops.value = true;
100
- wsSend({ type: 'list_loops' });
101
- }
102
-
103
- // ── Navigation ────────────────────────────────────
104
-
105
- function viewLoopDetail(loopId) {
106
- const loop = loopsList.value.find(l => l.id === loopId);
107
- if (!loop) return;
108
- selectedLoop.value = { ...loop };
109
- selectedExecution.value = null;
110
- executionMessages.value = [];
111
- executionHistory.value = [];
112
- loadingExecutions.value = true;
113
- editingLoopId.value = null;
114
- execPageLimit = 20;
115
- hasMoreExecutions.value = false;
116
- wsSend({ type: 'list_loop_executions', loopId, limit: execPageLimit });
117
- }
118
-
119
- function viewExecution(loopId, executionId) {
120
- selectedExecution.value = executionId;
121
- loadingExecution.value = true;
122
- executionMessages.value = [];
123
- wsSend({ type: 'get_loop_execution_messages', loopId, executionId });
124
- }
125
-
126
- function backToLoopsList() {
127
- selectedLoop.value = null;
128
- selectedExecution.value = null;
129
- executionHistory.value = [];
130
- executionMessages.value = [];
131
- editingLoopId.value = null;
132
- }
133
-
134
- function backToLoopDetail() {
135
- selectedExecution.value = null;
136
- executionMessages.value = [];
137
- }
138
-
139
- function startEditing(loopId) {
140
- editingLoopId.value = loopId;
141
- }
142
-
143
- function cancelEditing() {
144
- editingLoopId.value = null;
145
- }
146
-
147
- function loadMoreExecutions() {
148
- if (!selectedLoop.value || loadingMoreExecutions.value) return;
149
- loadingMoreExecutions.value = true;
150
- execPageLimit *= 2;
151
- wsSend({ type: 'list_loop_executions', loopId: selectedLoop.value.id, limit: execPageLimit });
152
- }
153
-
154
- function clearLoopError() {
155
- loopError.value = '';
156
- }
157
-
158
- // ── Live output accumulation ─────────────────────
159
-
160
- /** Message ID counter for live execution messages */
161
- let liveMsgIdCounter = 0;
162
-
163
- /**
164
- * Append a Claude output message to the live execution display.
165
- * Mirrors the team.js handleTeamAgentOutput accumulation logic.
166
- */
167
- function appendOutputToDisplay(data) {
168
- if (!data) return;
169
- const msgs = executionMessages.value;
170
-
171
- if (data.type === 'content_block_delta' && data.delta) {
172
- const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
173
- if (last && last.role === 'assistant' && last.isStreaming) {
174
- last.content += data.delta;
175
- } else {
176
- msgs.push({
177
- id: ++liveMsgIdCounter, role: 'assistant',
178
- content: data.delta, isStreaming: true, timestamp: Date.now(),
179
- });
180
- }
181
- } else if (data.type === 'tool_use' && data.tools) {
182
- const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
183
- if (last && last.role === 'assistant' && last.isStreaming) {
184
- last.isStreaming = false;
185
- }
186
- for (const tool of data.tools) {
187
- msgs.push({
188
- id: ++liveMsgIdCounter, role: 'tool',
189
- toolId: tool.id, toolName: tool.name || 'unknown',
190
- toolInput: tool.input ? JSON.stringify(tool.input, null, 2) : '',
191
- hasResult: false, expanded: true, timestamp: Date.now(),
192
- });
193
- }
194
- } else if (data.type === 'user' && data.tool_use_result) {
195
- const result = data.tool_use_result;
196
- const results = Array.isArray(result) ? result : [result];
197
- for (const r of results) {
198
- const toolMsg = msgs.find(m => m.role === 'tool' && m.toolId === r.tool_use_id);
199
- if (toolMsg) {
200
- toolMsg.toolOutput = typeof r.content === 'string'
201
- ? r.content : JSON.stringify(r.content, null, 2);
202
- toolMsg.hasResult = true;
203
- }
204
- }
205
- }
206
-
207
- scrollToBottom();
208
- }
209
-
210
- // ── Message routing ───────────────────────────────
211
-
212
- /**
213
- * Handle incoming Loop-related messages from the WebSocket.
214
- * Returns true if the message was consumed.
215
- */
216
- function handleLoopMessage(msg) {
217
- switch (msg.type) {
218
- case 'loops_list':
219
- loopsList.value = msg.loops || [];
220
- return true;
221
-
222
- case 'loop_created':
223
- loopsList.value.push(msg.loop);
224
- loopError.value = '';
225
- return true;
226
-
227
- case 'loop_updated': {
228
- const idx = loopsList.value.findIndex(l => l.id === msg.loop.id);
229
- if (idx >= 0) loopsList.value[idx] = msg.loop;
230
- if (selectedLoop.value?.id === msg.loop.id) {
231
- selectedLoop.value = { ...msg.loop };
232
- }
233
- editingLoopId.value = null;
234
- loopError.value = '';
235
- return true;
236
- }
237
-
238
- case 'loop_deleted':
239
- loopsList.value = loopsList.value.filter(l => l.id !== msg.loopId);
240
- if (selectedLoop.value?.id === msg.loopId) backToLoopsList();
241
- return true;
242
-
243
- case 'loop_execution_started':
244
- runningLoops.value = { ...runningLoops.value, [msg.loopId]: msg.execution };
245
- // If viewing this loop's detail, prepend to history
246
- if (selectedLoop.value?.id === msg.loopId) {
247
- executionHistory.value.unshift(msg.execution);
248
- }
249
- return true;
250
-
251
- case 'loop_execution_output':
252
- // If user is viewing this execution live, append to display
253
- if (selectedExecution.value === msg.executionId) {
254
- appendOutputToDisplay(msg.data);
255
- }
256
- return true;
257
-
258
- case 'loop_execution_completed': {
259
- const newRunning = { ...runningLoops.value };
260
- delete newRunning[msg.loopId];
261
- runningLoops.value = newRunning;
262
- // Update execution in history list
263
- if (selectedLoop.value?.id === msg.loopId) {
264
- const idx = executionHistory.value.findIndex(e => e.id === msg.execution.id);
265
- if (idx >= 0) executionHistory.value[idx] = msg.execution;
266
- }
267
- // Finalize streaming message
268
- const msgs = executionMessages.value;
269
- if (msgs.length > 0) {
270
- const last = msgs[msgs.length - 1];
271
- if (last.role === 'assistant' && last.isStreaming) {
272
- last.isStreaming = false;
273
- }
274
- }
275
- // Update Loop's lastExecution in sidebar list
276
- const loop = loopsList.value.find(l => l.id === msg.loopId);
277
- if (loop) {
278
- loop.lastExecution = {
279
- id: msg.execution.id,
280
- status: msg.execution.status,
281
- startedAt: msg.execution.startedAt,
282
- durationMs: msg.execution.durationMs,
283
- trigger: msg.execution.trigger,
284
- };
285
- }
286
- return true;
287
- }
288
-
289
- case 'loop_executions_list':
290
- if (selectedLoop.value?.id === msg.loopId) {
291
- const execs = msg.executions || [];
292
- executionHistory.value = execs;
293
- loadingExecutions.value = false;
294
- loadingMoreExecutions.value = false;
295
- hasMoreExecutions.value = execs.length >= execPageLimit;
296
- }
297
- return true;
298
-
299
- case 'loop_execution_messages':
300
- if (selectedExecution.value === msg.executionId) {
301
- if (msg.messages && msg.messages.length > 0) {
302
- let idCounter = 0;
303
- executionMessages.value = buildHistoryBatch(msg.messages, () => ++idCounter);
304
- liveMsgIdCounter = idCounter;
305
- } else {
306
- executionMessages.value = [];
307
- }
308
- loadingExecution.value = false;
309
- scrollToBottom();
310
- }
311
- return true;
312
-
313
- default:
314
- return false;
315
- }
316
- }
317
-
318
- return {
319
- // State
320
- loopsList, selectedLoop, selectedExecution,
321
- executionHistory, executionMessages, runningLoops,
322
- loadingExecutions, loadingExecution, editingLoopId,
323
- loopError, hasMoreExecutions, loadingMoreExecutions,
324
- // Computed
325
- hasRunningLoop, firstRunningLoop,
326
- // CRUD
327
- createNewLoop, updateExistingLoop, deleteExistingLoop,
328
- toggleLoop, runNow, cancelExecution, requestLoopsList,
329
- // Navigation
330
- viewLoopDetail, viewExecution,
331
- backToLoopsList, backToLoopDetail,
332
- startEditing, cancelEditing,
333
- // Pagination & errors
334
- loadMoreExecutions, clearLoopError,
335
- // Message routing
336
- handleLoopMessage,
337
- };
338
- }