@agent-link/server 0.1.187 → 0.1.189

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 (76) hide show
  1. package/dist/auth-manager.d.ts +36 -0
  2. package/dist/auth-manager.js +96 -0
  3. package/dist/auth-manager.js.map +1 -0
  4. package/dist/http.d.ts +4 -0
  5. package/dist/http.js +85 -0
  6. package/dist/http.js.map +1 -0
  7. package/dist/index.js +5 -84
  8. package/dist/index.js.map +1 -1
  9. package/dist/message-relay.d.ts +17 -0
  10. package/dist/message-relay.js +23 -0
  11. package/dist/message-relay.js.map +1 -0
  12. package/dist/session-manager.d.ts +44 -0
  13. package/dist/session-manager.js +83 -0
  14. package/dist/session-manager.js.map +1 -0
  15. package/dist/ws-agent.js +19 -27
  16. package/dist/ws-agent.js.map +1 -1
  17. package/dist/ws-client.js +31 -37
  18. package/dist/ws-client.js.map +1 -1
  19. package/package.json +3 -3
  20. package/web/dist/assets/index-DIO7Hox0.js +320 -0
  21. package/web/dist/assets/index-DIO7Hox0.js.map +1 -0
  22. package/web/dist/assets/index-Y1FN_mFe.css +1 -0
  23. package/web/{index.html → dist/index.html} +2 -19
  24. package/dist/auth.d.ts +0 -13
  25. package/dist/auth.js +0 -65
  26. package/dist/auth.js.map +0 -1
  27. package/dist/context.d.ts +0 -52
  28. package/dist/context.js +0 -60
  29. package/dist/context.js.map +0 -1
  30. package/web/app.js +0 -2881
  31. package/web/css/ask-question.css +0 -333
  32. package/web/css/base.css +0 -270
  33. package/web/css/btw.css +0 -148
  34. package/web/css/chat.css +0 -176
  35. package/web/css/file-browser.css +0 -499
  36. package/web/css/input.css +0 -671
  37. package/web/css/loop.css +0 -674
  38. package/web/css/markdown.css +0 -169
  39. package/web/css/responsive.css +0 -314
  40. package/web/css/sidebar.css +0 -593
  41. package/web/css/team.css +0 -1277
  42. package/web/css/tools.css +0 -327
  43. package/web/encryption.js +0 -56
  44. package/web/modules/appHelpers.js +0 -100
  45. package/web/modules/askQuestion.js +0 -63
  46. package/web/modules/backgroundRouting.js +0 -269
  47. package/web/modules/connection.js +0 -731
  48. package/web/modules/fileAttachments.js +0 -125
  49. package/web/modules/fileBrowser.js +0 -398
  50. package/web/modules/filePreview.js +0 -213
  51. package/web/modules/i18n.js +0 -101
  52. package/web/modules/loop.js +0 -338
  53. package/web/modules/loopTemplates.js +0 -110
  54. package/web/modules/markdown.js +0 -83
  55. package/web/modules/messageHelpers.js +0 -206
  56. package/web/modules/sidebar.js +0 -402
  57. package/web/modules/streaming.js +0 -116
  58. package/web/modules/team.js +0 -396
  59. package/web/modules/teamTemplates.js +0 -360
  60. package/web/vendor/highlight.min.js +0 -1213
  61. package/web/vendor/marked.min.js +0 -6
  62. package/web/vendor/nacl-fast.min.js +0 -1
  63. package/web/vendor/nacl-util.min.js +0 -1
  64. package/web/vendor/pako.min.js +0 -2
  65. package/web/vendor/vue.global.prod.js +0 -13
  66. /package/web/{favicon.svg → dist/favicon.svg} +0 -0
  67. /package/web/{images → dist/images}/chat-iPad.webp +0 -0
  68. /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
  69. /package/web/{images → dist/images}/loop-iPad.webp +0 -0
  70. /package/web/{images → dist/images}/team-iPad.webp +0 -0
  71. /package/web/{landing.html → dist/landing.html} +0 -0
  72. /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
  73. /package/web/{locales → dist/locales}/en.json +0 -0
  74. /package/web/{locales → dist/locales}/zh.json +0 -0
  75. /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
  76. /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
- }