@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/package.json +1 -1
- package/web/app.js +655 -59
- package/web/landing.html +3 -3
- package/web/landing.zh.html +3 -3
- 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
package/web/app.js
CHANGED
|
@@ -20,6 +20,8 @@ import { createFileBrowser } from './modules/fileBrowser.js';
|
|
|
20
20
|
import { createFilePreview } from './modules/filePreview.js';
|
|
21
21
|
import { createTeam } from './modules/team.js';
|
|
22
22
|
import { TEMPLATES, TEMPLATE_KEYS, buildFullLeadPrompt } from './modules/teamTemplates.js';
|
|
23
|
+
import { createLoop } from './modules/loop.js';
|
|
24
|
+
import { LOOP_TEMPLATES, LOOP_TEMPLATE_KEYS, buildCronExpression, formatSchedule } from './modules/loopTemplates.js';
|
|
23
25
|
import { createScrollManager, createHighlightScheduler, formatUsage } from './modules/appHelpers.js';
|
|
24
26
|
|
|
25
27
|
// ── App ─────────────────────────────────────────────────────────────────────
|
|
@@ -121,7 +123,10 @@ const App = {
|
|
|
121
123
|
const workdirMenuOpen = ref(false);
|
|
122
124
|
const teamsCollapsed = ref(false);
|
|
123
125
|
const chatsCollapsed = ref(false);
|
|
126
|
+
const loopsCollapsed = ref(false);
|
|
127
|
+
const _sidebarCollapseKey = () => hostname.value ? `agentlink-sidebar-collapsed-${hostname.value}` : null;
|
|
124
128
|
const loadingTeams = ref(false);
|
|
129
|
+
const loadingLoops = ref(false);
|
|
125
130
|
|
|
126
131
|
// Team creation state
|
|
127
132
|
const teamInstruction = ref('');
|
|
@@ -157,6 +162,21 @@ const App = {
|
|
|
157
162
|
const kanbanExpanded = ref(false);
|
|
158
163
|
const instructionExpanded = ref(false);
|
|
159
164
|
|
|
165
|
+
// Loop creation/editing form state
|
|
166
|
+
const loopName = ref('');
|
|
167
|
+
const loopPrompt = ref('');
|
|
168
|
+
const loopScheduleType = ref('daily');
|
|
169
|
+
const loopScheduleHour = ref(9);
|
|
170
|
+
const loopScheduleMinute = ref(0);
|
|
171
|
+
const loopScheduleDayOfWeek = ref(1);
|
|
172
|
+
const loopCronExpr = ref('0 9 * * *');
|
|
173
|
+
const loopSelectedTemplate = ref(null);
|
|
174
|
+
const loopDeleteConfirmOpen = ref(false);
|
|
175
|
+
const loopDeleteConfirmId = ref(null);
|
|
176
|
+
const loopDeleteConfirmName = ref('');
|
|
177
|
+
const renamingLoopId = ref(null);
|
|
178
|
+
const renameLoopText = ref('');
|
|
179
|
+
|
|
160
180
|
// File preview state
|
|
161
181
|
const previewPanelOpen = ref(false);
|
|
162
182
|
const previewPanelWidth = ref(parseInt(localStorage.getItem('agentlink-preview-panel-width'), 10) || 400);
|
|
@@ -278,7 +298,7 @@ const App = {
|
|
|
278
298
|
switchConversation,
|
|
279
299
|
});
|
|
280
300
|
|
|
281
|
-
const { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
|
|
301
|
+
const { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, setLoop, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
|
|
282
302
|
status, agentName, hostname, workDir, sessionId, error,
|
|
283
303
|
serverVersion, agentVersion, latency,
|
|
284
304
|
messages, isProcessing, isCompacting, visibleLimit, queuedMessages, usageStats,
|
|
@@ -305,8 +325,13 @@ const App = {
|
|
|
305
325
|
wsSend, scrollToBottom,
|
|
306
326
|
});
|
|
307
327
|
setTeam(team);
|
|
328
|
+
// Loop module
|
|
329
|
+
const loop = createLoop({
|
|
330
|
+
wsSend, scrollToBottom,
|
|
331
|
+
});
|
|
332
|
+
setLoop(loop);
|
|
308
333
|
sidebar.setOnSwitchToChat(() => {
|
|
309
|
-
team.
|
|
334
|
+
team.viewMode.value = 'chat';
|
|
310
335
|
team.historicalTeam.value = null;
|
|
311
336
|
});
|
|
312
337
|
|
|
@@ -462,9 +487,35 @@ const App = {
|
|
|
462
487
|
|
|
463
488
|
watch(hostname, (name) => {
|
|
464
489
|
document.title = name ? `${name} — AgentLink` : 'AgentLink';
|
|
490
|
+
// Restore sidebar collapsed states from localStorage
|
|
491
|
+
const key = _sidebarCollapseKey();
|
|
492
|
+
if (key) {
|
|
493
|
+
try {
|
|
494
|
+
const saved = JSON.parse(localStorage.getItem(key) || '{}');
|
|
495
|
+
if (saved.chats !== undefined) chatsCollapsed.value = saved.chats;
|
|
496
|
+
if (saved.teams !== undefined) teamsCollapsed.value = saved.teams;
|
|
497
|
+
if (saved.loops !== undefined) loopsCollapsed.value = saved.loops;
|
|
498
|
+
} catch (_) { /* ignore */ }
|
|
499
|
+
}
|
|
465
500
|
});
|
|
466
501
|
|
|
502
|
+
// Persist sidebar collapsed states to localStorage
|
|
503
|
+
const _saveSidebarCollapsed = () => {
|
|
504
|
+
const key = _sidebarCollapseKey();
|
|
505
|
+
if (key) {
|
|
506
|
+
localStorage.setItem(key, JSON.stringify({
|
|
507
|
+
chats: chatsCollapsed.value,
|
|
508
|
+
teams: teamsCollapsed.value,
|
|
509
|
+
loops: loopsCollapsed.value,
|
|
510
|
+
}));
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
watch(chatsCollapsed, _saveSidebarCollapsed);
|
|
514
|
+
watch(teamsCollapsed, _saveSidebarCollapsed);
|
|
515
|
+
watch(loopsCollapsed, _saveSidebarCollapsed);
|
|
516
|
+
|
|
467
517
|
watch(team.teamsList, () => { loadingTeams.value = false; });
|
|
518
|
+
watch(loop.loopsList, () => { loadingLoops.value = false; });
|
|
468
519
|
|
|
469
520
|
// ── Lifecycle ──
|
|
470
521
|
onMounted(() => { connect(scheduleHighlight); });
|
|
@@ -579,7 +630,7 @@ const App = {
|
|
|
579
630
|
// File preview
|
|
580
631
|
previewPanelOpen, previewPanelWidth, previewFile, previewLoading, previewMarkdownRendered, filePreview,
|
|
581
632
|
workdirMenuOpen,
|
|
582
|
-
teamsCollapsed, chatsCollapsed, loadingTeams,
|
|
633
|
+
teamsCollapsed, chatsCollapsed, loopsCollapsed, loadingTeams, loadingLoops,
|
|
583
634
|
toggleWorkdirMenu() { workdirMenuOpen.value = !workdirMenuOpen.value; },
|
|
584
635
|
workdirMenuBrowse() {
|
|
585
636
|
workdirMenuOpen.value = false;
|
|
@@ -597,7 +648,7 @@ const App = {
|
|
|
597
648
|
// Team mode
|
|
598
649
|
team,
|
|
599
650
|
teamState: team.teamState,
|
|
600
|
-
|
|
651
|
+
viewMode: team.viewMode,
|
|
601
652
|
activeAgentView: team.activeAgentView,
|
|
602
653
|
historicalTeam: team.historicalTeam,
|
|
603
654
|
teamsList: team.teamsList,
|
|
@@ -709,6 +760,206 @@ const App = {
|
|
|
709
760
|
}
|
|
710
761
|
return '';
|
|
711
762
|
},
|
|
763
|
+
// Loop mode
|
|
764
|
+
loop,
|
|
765
|
+
loopsList: loop.loopsList,
|
|
766
|
+
selectedLoop: loop.selectedLoop,
|
|
767
|
+
selectedExecution: loop.selectedExecution,
|
|
768
|
+
executionHistory: loop.executionHistory,
|
|
769
|
+
executionMessages: loop.executionMessages,
|
|
770
|
+
runningLoops: loop.runningLoops,
|
|
771
|
+
loadingExecutions: loop.loadingExecutions,
|
|
772
|
+
loadingExecution: loop.loadingExecution,
|
|
773
|
+
editingLoopId: loop.editingLoopId,
|
|
774
|
+
hasRunningLoop: loop.hasRunningLoop,
|
|
775
|
+
firstRunningLoop: loop.firstRunningLoop,
|
|
776
|
+
loopError: loop.loopError,
|
|
777
|
+
hasMoreExecutions: loop.hasMoreExecutions,
|
|
778
|
+
loadingMoreExecutions: loop.loadingMoreExecutions,
|
|
779
|
+
toggleLoop: loop.toggleLoop,
|
|
780
|
+
runNow: loop.runNow,
|
|
781
|
+
cancelLoopExecution: loop.cancelExecution,
|
|
782
|
+
viewLoopDetail: loop.viewLoopDetail,
|
|
783
|
+
viewExecution: loop.viewExecution,
|
|
784
|
+
backToLoopsList: loop.backToLoopsList,
|
|
785
|
+
backToLoopDetail: loop.backToLoopDetail,
|
|
786
|
+
LOOP_TEMPLATES, LOOP_TEMPLATE_KEYS,
|
|
787
|
+
buildCronExpression, formatSchedule,
|
|
788
|
+
// Loop form state
|
|
789
|
+
loopName, loopPrompt, loopScheduleType,
|
|
790
|
+
loopScheduleHour, loopScheduleMinute, loopScheduleDayOfWeek,
|
|
791
|
+
loopCronExpr, loopSelectedTemplate,
|
|
792
|
+
loopDeleteConfirmOpen, loopDeleteConfirmId, loopDeleteConfirmName,
|
|
793
|
+
renamingLoopId, renameLoopText,
|
|
794
|
+
startLoopRename(l) {
|
|
795
|
+
renamingLoopId.value = l.id;
|
|
796
|
+
renameLoopText.value = l.name || '';
|
|
797
|
+
},
|
|
798
|
+
confirmLoopRename() {
|
|
799
|
+
const lid = renamingLoopId.value;
|
|
800
|
+
const name = renameLoopText.value.trim();
|
|
801
|
+
if (!lid || !name) { renamingLoopId.value = null; renameLoopText.value = ''; return; }
|
|
802
|
+
loop.updateExistingLoop(lid, { name });
|
|
803
|
+
renamingLoopId.value = null;
|
|
804
|
+
renameLoopText.value = '';
|
|
805
|
+
},
|
|
806
|
+
cancelLoopRename() {
|
|
807
|
+
renamingLoopId.value = null;
|
|
808
|
+
renameLoopText.value = '';
|
|
809
|
+
},
|
|
810
|
+
requestLoopsList() {
|
|
811
|
+
loadingLoops.value = true;
|
|
812
|
+
loop.requestLoopsList();
|
|
813
|
+
},
|
|
814
|
+
newLoop() {
|
|
815
|
+
loop.backToLoopsList();
|
|
816
|
+
loop.editingLoopId.value = null;
|
|
817
|
+
loopSelectedTemplate.value = null;
|
|
818
|
+
loopName.value = '';
|
|
819
|
+
loopPrompt.value = '';
|
|
820
|
+
loopScheduleType.value = 'daily';
|
|
821
|
+
loopScheduleHour.value = 9;
|
|
822
|
+
loopScheduleMinute.value = 0;
|
|
823
|
+
loopScheduleDayOfWeek.value = 1;
|
|
824
|
+
loopCronExpr.value = '0 9 * * *';
|
|
825
|
+
team.viewMode.value = 'loop';
|
|
826
|
+
},
|
|
827
|
+
viewLoop(loopId) {
|
|
828
|
+
loop.viewLoopDetail(loopId);
|
|
829
|
+
team.viewMode.value = 'loop';
|
|
830
|
+
},
|
|
831
|
+
selectLoopTemplate(key) {
|
|
832
|
+
loopSelectedTemplate.value = key;
|
|
833
|
+
const tpl = LOOP_TEMPLATES[key];
|
|
834
|
+
if (!tpl) return;
|
|
835
|
+
loopName.value = tpl.name || '';
|
|
836
|
+
loopPrompt.value = tpl.prompt || '';
|
|
837
|
+
loopScheduleType.value = tpl.scheduleType || 'daily';
|
|
838
|
+
const cfg = tpl.scheduleConfig || {};
|
|
839
|
+
loopScheduleHour.value = cfg.hour ?? 9;
|
|
840
|
+
loopScheduleMinute.value = cfg.minute ?? 0;
|
|
841
|
+
loopScheduleDayOfWeek.value = cfg.dayOfWeek ?? 1;
|
|
842
|
+
loopCronExpr.value = buildCronExpression(tpl.scheduleType || 'daily', cfg);
|
|
843
|
+
},
|
|
844
|
+
resetLoopForm() {
|
|
845
|
+
loopSelectedTemplate.value = null;
|
|
846
|
+
loopName.value = '';
|
|
847
|
+
loopPrompt.value = '';
|
|
848
|
+
loopScheduleType.value = 'daily';
|
|
849
|
+
loopScheduleHour.value = 9;
|
|
850
|
+
loopScheduleMinute.value = 0;
|
|
851
|
+
loopScheduleDayOfWeek.value = 1;
|
|
852
|
+
loopCronExpr.value = '0 9 * * *';
|
|
853
|
+
loop.editingLoopId.value = null;
|
|
854
|
+
},
|
|
855
|
+
createLoopFromPanel() {
|
|
856
|
+
const name = loopName.value.trim();
|
|
857
|
+
const prompt = loopPrompt.value.trim();
|
|
858
|
+
if (!name || !prompt) return;
|
|
859
|
+
loop.clearLoopError();
|
|
860
|
+
const schedCfg = { hour: loopScheduleHour.value, minute: loopScheduleMinute.value };
|
|
861
|
+
if (loopScheduleType.value === 'weekly') schedCfg.dayOfWeek = loopScheduleDayOfWeek.value;
|
|
862
|
+
if (loopScheduleType.value === 'cron') schedCfg.cronExpression = loopCronExpr.value;
|
|
863
|
+
const schedule = loopScheduleType.value === 'manual' ? ''
|
|
864
|
+
: loopScheduleType.value === 'cron' ? loopCronExpr.value
|
|
865
|
+
: buildCronExpression(loopScheduleType.value, schedCfg);
|
|
866
|
+
loop.createNewLoop({ name, prompt, schedule, scheduleType: loopScheduleType.value, scheduleConfig: schedCfg });
|
|
867
|
+
// Reset form
|
|
868
|
+
loopSelectedTemplate.value = null;
|
|
869
|
+
loopName.value = '';
|
|
870
|
+
loopPrompt.value = '';
|
|
871
|
+
loopScheduleType.value = 'daily';
|
|
872
|
+
loopScheduleHour.value = 9;
|
|
873
|
+
loopScheduleMinute.value = 0;
|
|
874
|
+
loopScheduleDayOfWeek.value = 1;
|
|
875
|
+
loopCronExpr.value = '0 9 * * *';
|
|
876
|
+
},
|
|
877
|
+
startEditingLoop(l) {
|
|
878
|
+
loop.editingLoopId.value = l.id;
|
|
879
|
+
loopName.value = l.name || '';
|
|
880
|
+
loopPrompt.value = l.prompt || '';
|
|
881
|
+
loopScheduleType.value = l.scheduleType || 'daily';
|
|
882
|
+
const cfg = l.scheduleConfig || {};
|
|
883
|
+
loopScheduleHour.value = cfg.hour ?? 9;
|
|
884
|
+
loopScheduleMinute.value = cfg.minute ?? 0;
|
|
885
|
+
loopScheduleDayOfWeek.value = cfg.dayOfWeek ?? 1;
|
|
886
|
+
loopCronExpr.value = l.schedule || buildCronExpression(l.scheduleType || 'daily', cfg);
|
|
887
|
+
},
|
|
888
|
+
saveLoopEdits() {
|
|
889
|
+
const lid = loop.editingLoopId.value;
|
|
890
|
+
if (!lid) return;
|
|
891
|
+
const name = loopName.value.trim();
|
|
892
|
+
const prompt = loopPrompt.value.trim();
|
|
893
|
+
if (!name || !prompt) return;
|
|
894
|
+
loop.clearLoopError();
|
|
895
|
+
const schedCfg = { hour: loopScheduleHour.value, minute: loopScheduleMinute.value };
|
|
896
|
+
if (loopScheduleType.value === 'weekly') schedCfg.dayOfWeek = loopScheduleDayOfWeek.value;
|
|
897
|
+
if (loopScheduleType.value === 'cron') schedCfg.cronExpression = loopCronExpr.value;
|
|
898
|
+
const schedule = loopScheduleType.value === 'manual' ? ''
|
|
899
|
+
: loopScheduleType.value === 'cron' ? loopCronExpr.value
|
|
900
|
+
: buildCronExpression(loopScheduleType.value, schedCfg);
|
|
901
|
+
loop.updateExistingLoop(lid, { name, prompt, schedule, scheduleType: loopScheduleType.value, scheduleConfig: schedCfg });
|
|
902
|
+
loop.editingLoopId.value = null;
|
|
903
|
+
loopName.value = '';
|
|
904
|
+
loopPrompt.value = '';
|
|
905
|
+
},
|
|
906
|
+
cancelEditingLoop() {
|
|
907
|
+
loop.editingLoopId.value = null;
|
|
908
|
+
loopName.value = '';
|
|
909
|
+
loopPrompt.value = '';
|
|
910
|
+
loopScheduleType.value = 'daily';
|
|
911
|
+
loopScheduleHour.value = 9;
|
|
912
|
+
loopScheduleMinute.value = 0;
|
|
913
|
+
},
|
|
914
|
+
requestDeleteLoop(l) {
|
|
915
|
+
loopDeleteConfirmId.value = l.id;
|
|
916
|
+
loopDeleteConfirmName.value = l.name || l.id.slice(0, 8);
|
|
917
|
+
loopDeleteConfirmOpen.value = true;
|
|
918
|
+
},
|
|
919
|
+
confirmDeleteLoop() {
|
|
920
|
+
if (!loopDeleteConfirmId.value) return;
|
|
921
|
+
loop.deleteExistingLoop(loopDeleteConfirmId.value);
|
|
922
|
+
loopDeleteConfirmOpen.value = false;
|
|
923
|
+
loopDeleteConfirmId.value = null;
|
|
924
|
+
},
|
|
925
|
+
cancelDeleteLoop() {
|
|
926
|
+
loopDeleteConfirmOpen.value = false;
|
|
927
|
+
loopDeleteConfirmId.value = null;
|
|
928
|
+
},
|
|
929
|
+
loadMoreExecutions() {
|
|
930
|
+
loop.loadMoreExecutions();
|
|
931
|
+
},
|
|
932
|
+
clearLoopError() {
|
|
933
|
+
loop.clearLoopError();
|
|
934
|
+
},
|
|
935
|
+
loopScheduleDisplay(l) {
|
|
936
|
+
return formatSchedule(l.scheduleType, l.scheduleConfig || {}, l.schedule);
|
|
937
|
+
},
|
|
938
|
+
loopLastRunDisplay(l) {
|
|
939
|
+
if (!l.lastExecution) return '';
|
|
940
|
+
const exec = l.lastExecution;
|
|
941
|
+
const ago = formatRelativeTime(exec.startedAt);
|
|
942
|
+
const icon = exec.status === 'success' ? 'OK' : exec.status === 'error' ? 'ERR' : exec.status;
|
|
943
|
+
return ago + ' ' + icon;
|
|
944
|
+
},
|
|
945
|
+
formatExecTime(ts) {
|
|
946
|
+
if (!ts) return '';
|
|
947
|
+
const d = new Date(ts);
|
|
948
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ', ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
949
|
+
},
|
|
950
|
+
formatDuration(ms) {
|
|
951
|
+
if (!ms && ms !== 0) return '';
|
|
952
|
+
const secs = Math.floor(ms / 1000);
|
|
953
|
+
const m = Math.floor(secs / 60);
|
|
954
|
+
const s = secs % 60;
|
|
955
|
+
return m + 'm ' + String(s).padStart(2, '0') + 's';
|
|
956
|
+
},
|
|
957
|
+
isLoopRunning(loopId) {
|
|
958
|
+
return !!loop.runningLoops.value[loopId];
|
|
959
|
+
},
|
|
960
|
+
padTwo(n) {
|
|
961
|
+
return String(n).padStart(2, '0');
|
|
962
|
+
},
|
|
712
963
|
};
|
|
713
964
|
},
|
|
714
965
|
template: `
|
|
@@ -725,9 +976,15 @@ const App = {
|
|
|
725
976
|
<span v-if="latency !== null && status === 'Connected'" class="latency" :class="{ good: latency < 100, ok: latency >= 100 && latency < 500, bad: latency >= 500 }">{{ latency }}ms</span>
|
|
726
977
|
<span v-if="agentName" class="agent-label">{{ agentName }}</span>
|
|
727
978
|
<div class="team-mode-toggle">
|
|
728
|
-
<button :class="['team-mode-btn', { active:
|
|
729
|
-
<button :class="['team-mode-btn', { active:
|
|
979
|
+
<button :class="['team-mode-btn', { active: viewMode === 'chat' }]" @click="viewMode = 'chat'">Chat</button>
|
|
980
|
+
<button :class="['team-mode-btn', { active: viewMode === 'team' }]" @click="viewMode = 'team'">Team</button>
|
|
981
|
+
<button :class="['team-mode-btn', { active: viewMode === 'loop' }]" @click="viewMode = 'loop'">Loop</button>
|
|
730
982
|
</div>
|
|
983
|
+
<select class="team-mode-select" :value="viewMode" @change="viewMode = $event.target.value">
|
|
984
|
+
<option value="chat">Chat</option>
|
|
985
|
+
<option value="team">Team</option>
|
|
986
|
+
<option value="loop">Loop</option>
|
|
987
|
+
</select>
|
|
731
988
|
<button class="theme-toggle" @click="toggleTheme" :title="theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
|
|
732
989
|
<svg v-if="theme === 'dark'" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>
|
|
733
990
|
<svg v-else viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/></svg>
|
|
@@ -887,8 +1144,86 @@ const App = {
|
|
|
887
1144
|
</div>
|
|
888
1145
|
</div>
|
|
889
1146
|
|
|
1147
|
+
<!-- Chat History section -->
|
|
1148
|
+
<div class="sidebar-section sidebar-sessions" :style="{ flex: chatsCollapsed ? '0 0 auto' : '1 1 0', minHeight: chatsCollapsed ? 'auto' : '0' }">
|
|
1149
|
+
<div class="sidebar-section-header" @click="chatsCollapsed = !chatsCollapsed" style="cursor: pointer;">
|
|
1150
|
+
<span>Chat History</span>
|
|
1151
|
+
<span class="sidebar-section-header-actions">
|
|
1152
|
+
<button class="sidebar-refresh-btn" @click.stop="requestSessionList" title="Refresh" :disabled="loadingSessions">
|
|
1153
|
+
<svg :class="{ spinning: loadingSessions }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
1154
|
+
</button>
|
|
1155
|
+
<button class="sidebar-collapse-btn" :title="chatsCollapsed ? 'Expand' : 'Collapse'">
|
|
1156
|
+
<svg :class="{ collapsed: chatsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
1157
|
+
</button>
|
|
1158
|
+
</span>
|
|
1159
|
+
</div>
|
|
1160
|
+
|
|
1161
|
+
<div v-show="!chatsCollapsed" class="sidebar-section-collapsible">
|
|
1162
|
+
<button class="new-conversation-btn" @click="newConversation">
|
|
1163
|
+
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
1164
|
+
New conversation
|
|
1165
|
+
</button>
|
|
1166
|
+
|
|
1167
|
+
<div v-if="loadingSessions && historySessions.length === 0" class="sidebar-loading">
|
|
1168
|
+
Loading sessions...
|
|
1169
|
+
</div>
|
|
1170
|
+
<div v-else-if="historySessions.length === 0" class="sidebar-empty">
|
|
1171
|
+
No previous sessions found.
|
|
1172
|
+
</div>
|
|
1173
|
+
<div v-else class="session-list">
|
|
1174
|
+
<div v-for="group in groupedSessions" :key="group.label" class="session-group">
|
|
1175
|
+
<div class="session-group-label">{{ group.label }}</div>
|
|
1176
|
+
<div
|
|
1177
|
+
v-for="s in group.sessions" :key="s.sessionId"
|
|
1178
|
+
:class="['session-item', { active: currentClaudeSessionId === s.sessionId, processing: isSessionProcessing(s.sessionId) }]"
|
|
1179
|
+
@click="renamingSessionId !== s.sessionId && resumeSession(s)"
|
|
1180
|
+
:title="s.preview"
|
|
1181
|
+
:aria-label="(s.title || s.sessionId.slice(0, 8)) + (isSessionProcessing(s.sessionId) ? ' (processing)' : '')"
|
|
1182
|
+
>
|
|
1183
|
+
<div v-if="renamingSessionId === s.sessionId" class="session-rename-row">
|
|
1184
|
+
<input
|
|
1185
|
+
class="session-rename-input"
|
|
1186
|
+
v-model="renameText"
|
|
1187
|
+
@click.stop
|
|
1188
|
+
@keydown.enter.stop="confirmRename"
|
|
1189
|
+
@keydown.escape.stop="cancelRename"
|
|
1190
|
+
@vue:mounted="$event.el.focus()"
|
|
1191
|
+
/>
|
|
1192
|
+
<button class="session-rename-ok" @click.stop="confirmRename" title="Confirm">✓</button>
|
|
1193
|
+
<button class="session-rename-cancel" @click.stop="cancelRename" title="Cancel">×</button>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div v-else class="session-title">
|
|
1196
|
+
<svg v-if="s.title && s.title.startsWith('You are a team lead')" class="session-team-icon" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
|
1197
|
+
{{ s.title }}
|
|
1198
|
+
</div>
|
|
1199
|
+
<div class="session-meta">
|
|
1200
|
+
<span>{{ formatRelativeTime(s.lastModified) }}</span>
|
|
1201
|
+
<span v-if="renamingSessionId !== s.sessionId" class="session-actions">
|
|
1202
|
+
<button
|
|
1203
|
+
class="session-rename-btn"
|
|
1204
|
+
@click.stop="startRename(s)"
|
|
1205
|
+
title="Rename session"
|
|
1206
|
+
>
|
|
1207
|
+
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
1208
|
+
</button>
|
|
1209
|
+
<button
|
|
1210
|
+
v-if="currentClaudeSessionId !== s.sessionId"
|
|
1211
|
+
class="session-delete-btn"
|
|
1212
|
+
@click.stop="deleteSession(s)"
|
|
1213
|
+
title="Delete session"
|
|
1214
|
+
>
|
|
1215
|
+
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
1216
|
+
</button>
|
|
1217
|
+
</span>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
|
|
890
1225
|
<!-- Teams section -->
|
|
891
|
-
<div class="sidebar-section sidebar-teams">
|
|
1226
|
+
<div class="sidebar-section sidebar-teams" :style="{ flex: teamsCollapsed ? '0 0 auto' : '1 1 0', minHeight: teamsCollapsed ? 'auto' : '0' }">
|
|
892
1227
|
<div class="sidebar-section-header" @click="teamsCollapsed = !teamsCollapsed" style="cursor: pointer;">
|
|
893
1228
|
<span>Teams History</span>
|
|
894
1229
|
<span class="sidebar-section-header-actions">
|
|
@@ -947,73 +1282,58 @@ const App = {
|
|
|
947
1282
|
</div>
|
|
948
1283
|
</div>
|
|
949
1284
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1285
|
+
<!-- Loops section -->
|
|
1286
|
+
<div class="sidebar-section sidebar-loops" :style="{ flex: loopsCollapsed ? '0 0 auto' : '1 1 0', minHeight: loopsCollapsed ? 'auto' : '0' }">
|
|
1287
|
+
<div class="sidebar-section-header" @click="loopsCollapsed = !loopsCollapsed" style="cursor: pointer;">
|
|
1288
|
+
<span>Loops</span>
|
|
953
1289
|
<span class="sidebar-section-header-actions">
|
|
954
|
-
<button class="sidebar-refresh-btn" @click.stop="
|
|
955
|
-
<svg :class="{ spinning:
|
|
1290
|
+
<button class="sidebar-refresh-btn" @click.stop="requestLoopsList" title="Refresh" :disabled="loadingLoops">
|
|
1291
|
+
<svg :class="{ spinning: loadingLoops }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
|
956
1292
|
</button>
|
|
957
|
-
<button class="sidebar-collapse-btn" :title="
|
|
958
|
-
<svg :class="{ collapsed:
|
|
1293
|
+
<button class="sidebar-collapse-btn" :title="loopsCollapsed ? 'Expand' : 'Collapse'">
|
|
1294
|
+
<svg :class="{ collapsed: loopsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
|
|
959
1295
|
</button>
|
|
960
1296
|
</span>
|
|
961
1297
|
</div>
|
|
962
1298
|
|
|
963
|
-
<div v-show="!
|
|
964
|
-
<button class="new-conversation-btn" @click="
|
|
1299
|
+
<div v-show="!loopsCollapsed" class="sidebar-section-collapsible">
|
|
1300
|
+
<button class="new-conversation-btn" @click="newLoop">
|
|
965
1301
|
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
966
|
-
New
|
|
1302
|
+
New loop
|
|
967
1303
|
</button>
|
|
968
1304
|
|
|
969
|
-
<div v-if="
|
|
970
|
-
|
|
1305
|
+
<div v-if="loopsList.length === 0 && !loadingLoops" class="sidebar-empty">
|
|
1306
|
+
No loops configured.
|
|
971
1307
|
</div>
|
|
972
|
-
<div v-else
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
@click="renamingSessionId !== s.sessionId && resumeSession(s)"
|
|
982
|
-
:title="s.preview"
|
|
983
|
-
:aria-label="(s.title || s.sessionId.slice(0, 8)) + (isSessionProcessing(s.sessionId) ? ' (processing)' : '')"
|
|
984
|
-
>
|
|
985
|
-
<div v-if="renamingSessionId === s.sessionId" class="session-rename-row">
|
|
1308
|
+
<div v-else class="loop-history-list">
|
|
1309
|
+
<div
|
|
1310
|
+
v-for="l in loopsList" :key="l.id"
|
|
1311
|
+
:class="['team-history-item', { active: selectedLoop?.id === l.id }]"
|
|
1312
|
+
@click="renamingLoopId !== l.id && viewLoop(l.id)"
|
|
1313
|
+
:title="l.name"
|
|
1314
|
+
>
|
|
1315
|
+
<div class="team-history-info">
|
|
1316
|
+
<div v-if="renamingLoopId === l.id" class="session-rename-row">
|
|
986
1317
|
<input
|
|
987
1318
|
class="session-rename-input"
|
|
988
|
-
v-model="
|
|
1319
|
+
v-model="renameLoopText"
|
|
989
1320
|
@click.stop
|
|
990
|
-
@keydown.enter.stop="
|
|
991
|
-
@keydown.escape.stop="
|
|
1321
|
+
@keydown.enter.stop="confirmLoopRename"
|
|
1322
|
+
@keydown.escape.stop="cancelLoopRename"
|
|
992
1323
|
@vue:mounted="$event.el.focus()"
|
|
993
1324
|
/>
|
|
994
|
-
<button class="session-rename-ok" @click.stop="
|
|
995
|
-
<button class="session-rename-cancel" @click.stop="
|
|
996
|
-
</div>
|
|
997
|
-
<div v-else class="session-title">
|
|
998
|
-
<svg v-if="s.title && s.title.startsWith('You are a team lead')" class="session-team-icon" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
|
999
|
-
{{ s.title }}
|
|
1325
|
+
<button class="session-rename-ok" @click.stop="confirmLoopRename" title="Confirm">✓</button>
|
|
1326
|
+
<button class="session-rename-cancel" @click.stop="cancelLoopRename" title="Cancel">×</button>
|
|
1000
1327
|
</div>
|
|
1001
|
-
<div class="
|
|
1002
|
-
|
|
1003
|
-
<span
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
title="Rename session"
|
|
1008
|
-
>
|
|
1328
|
+
<div v-else class="team-history-title">{{ l.name || 'Untitled loop' }}</div>
|
|
1329
|
+
<div v-if="renamingLoopId !== l.id" class="team-history-meta">
|
|
1330
|
+
<span :class="['team-status-badge', 'team-status-badge-sm', l.enabled ? 'team-status-running' : 'team-status-completed']">{{ l.enabled ? 'active' : 'paused' }}</span>
|
|
1331
|
+
<span v-if="l.scheduleType" class="team-history-tasks">{{ formatSchedule(l.scheduleType, l.scheduleConfig || {}, l.schedule) }}</span>
|
|
1332
|
+
<span class="session-actions">
|
|
1333
|
+
<button class="session-rename-btn" @click.stop="startLoopRename(l)" title="Rename loop">
|
|
1009
1334
|
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
1010
1335
|
</button>
|
|
1011
|
-
<button
|
|
1012
|
-
v-if="currentClaudeSessionId !== s.sessionId"
|
|
1013
|
-
class="session-delete-btn"
|
|
1014
|
-
@click.stop="deleteSession(s)"
|
|
1015
|
-
title="Delete session"
|
|
1016
|
-
>
|
|
1336
|
+
<button class="session-delete-btn" @click.stop="requestDeleteLoop(l)" title="Delete loop">
|
|
1017
1337
|
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
1018
1338
|
</button>
|
|
1019
1339
|
</span>
|
|
@@ -1079,7 +1399,7 @@ const App = {
|
|
|
1079
1399
|
<div class="chat-area">
|
|
1080
1400
|
|
|
1081
1401
|
<!-- ══ Team Dashboard ══ -->
|
|
1082
|
-
<template v-if="
|
|
1402
|
+
<template v-if="viewMode === 'team'">
|
|
1083
1403
|
|
|
1084
1404
|
<!-- Team creation panel (no active team) -->
|
|
1085
1405
|
<div v-if="!displayTeam" class="team-create-panel">
|
|
@@ -1425,8 +1745,284 @@ const App = {
|
|
|
1425
1745
|
</div>
|
|
1426
1746
|
</template>
|
|
1427
1747
|
|
|
1748
|
+
<!-- ══ Loop Dashboard ══ -->
|
|
1749
|
+
<template v-else-if="viewMode === 'loop'">
|
|
1750
|
+
|
|
1751
|
+
<!-- ── Execution detail view ── -->
|
|
1752
|
+
<div v-if="selectedLoop && selectedExecution" class="team-create-panel">
|
|
1753
|
+
<div class="team-create-inner">
|
|
1754
|
+
<div class="loop-detail-header">
|
|
1755
|
+
<button class="team-agent-back-btn" @click="backToLoopDetail()">
|
|
1756
|
+
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
|
1757
|
+
{{ selectedLoop.name }}
|
|
1758
|
+
</button>
|
|
1759
|
+
</div>
|
|
1760
|
+
|
|
1761
|
+
<div v-if="loadingExecution" class="loop-loading">
|
|
1762
|
+
<div class="history-loading-spinner"></div>
|
|
1763
|
+
<span>Loading execution...</span>
|
|
1764
|
+
</div>
|
|
1765
|
+
|
|
1766
|
+
<div v-else class="loop-exec-messages">
|
|
1767
|
+
<div v-if="executionMessages.length === 0" class="team-agent-empty-msg">No messages recorded for this execution.</div>
|
|
1768
|
+
<template v-for="(msg, mi) in executionMessages" :key="msg.id">
|
|
1769
|
+
<div v-if="msg.role === 'user' && msg.content" class="team-agent-prompt">
|
|
1770
|
+
<div class="team-agent-prompt-label">Loop Prompt</div>
|
|
1771
|
+
<div class="team-agent-prompt-body markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
1772
|
+
</div>
|
|
1773
|
+
<div v-else-if="msg.role === 'assistant'" :class="['message', 'message-assistant']">
|
|
1774
|
+
<div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]">
|
|
1775
|
+
<div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
1776
|
+
</div>
|
|
1777
|
+
</div>
|
|
1778
|
+
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
1779
|
+
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
1780
|
+
<span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
|
|
1781
|
+
<span class="tool-name">{{ msg.toolName }}</span>
|
|
1782
|
+
<span class="tool-summary">{{ getToolSummary(msg) }}</span>
|
|
1783
|
+
<span class="tool-status-icon" v-if="msg.hasResult">\u{2713}</span>
|
|
1784
|
+
<span class="tool-status-icon running-dots" v-else>
|
|
1785
|
+
<span></span><span></span><span></span>
|
|
1786
|
+
</span>
|
|
1787
|
+
<span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
|
|
1788
|
+
</div>
|
|
1789
|
+
<div v-show="msg.expanded" class="tool-expand">
|
|
1790
|
+
<div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
|
|
1791
|
+
<div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
|
|
1792
|
+
<pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
|
|
1793
|
+
<pre v-if="msg.toolOutput" class="tool-block tool-output">{{ msg.toolOutput }}</pre>
|
|
1794
|
+
</div>
|
|
1795
|
+
</div>
|
|
1796
|
+
</template>
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
|
|
1801
|
+
<!-- ── Loop detail view (execution history) ── -->
|
|
1802
|
+
<div v-else-if="selectedLoop" class="team-create-panel">
|
|
1803
|
+
<div class="team-create-inner">
|
|
1804
|
+
<div class="loop-detail-header">
|
|
1805
|
+
<button class="team-agent-back-btn" @click="backToLoopsList()">
|
|
1806
|
+
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
|
1807
|
+
Back to Loops
|
|
1808
|
+
</button>
|
|
1809
|
+
</div>
|
|
1810
|
+
<div class="loop-detail-info">
|
|
1811
|
+
<h2 class="loop-detail-name">{{ selectedLoop.name }}</h2>
|
|
1812
|
+
<div class="loop-detail-meta">
|
|
1813
|
+
<span class="loop-detail-schedule">{{ loopScheduleDisplay(selectedLoop) }}</span>
|
|
1814
|
+
<span :class="['loop-status-badge', selectedLoop.enabled ? 'loop-status-enabled' : 'loop-status-disabled']">{{ selectedLoop.enabled ? 'Enabled' : 'Disabled' }}</span>
|
|
1815
|
+
</div>
|
|
1816
|
+
<div class="loop-detail-actions">
|
|
1817
|
+
<button class="loop-action-btn" @click="startEditingLoop(selectedLoop); selectedLoop = null">Edit</button>
|
|
1818
|
+
<button class="loop-action-btn loop-action-run" @click="runNow(selectedLoop.id)" :disabled="isLoopRunning(selectedLoop.id)">Run Now</button>
|
|
1819
|
+
<button class="loop-action-btn" @click="toggleLoop(selectedLoop.id)">{{ selectedLoop.enabled ? 'Disable' : 'Enable' }}</button>
|
|
1820
|
+
</div>
|
|
1821
|
+
</div>
|
|
1822
|
+
|
|
1823
|
+
<div class="loop-detail-prompt-section">
|
|
1824
|
+
<div class="loop-detail-prompt-label">Prompt</div>
|
|
1825
|
+
<div class="loop-detail-prompt-text">{{ selectedLoop.prompt }}</div>
|
|
1826
|
+
</div>
|
|
1827
|
+
|
|
1828
|
+
<div class="loop-exec-history-section">
|
|
1829
|
+
<div class="loop-exec-history-header">Execution History</div>
|
|
1830
|
+
<div v-if="loadingExecutions" class="loop-loading">
|
|
1831
|
+
<div class="history-loading-spinner"></div>
|
|
1832
|
+
<span>Loading executions...</span>
|
|
1833
|
+
</div>
|
|
1834
|
+
<div v-else-if="executionHistory.length === 0" class="loop-exec-empty">No executions yet.</div>
|
|
1835
|
+
<div v-else class="loop-exec-list">
|
|
1836
|
+
<div v-for="exec in executionHistory" :key="exec.id" class="loop-exec-item">
|
|
1837
|
+
<div class="loop-exec-item-left">
|
|
1838
|
+
<span :class="['loop-exec-status-icon', 'loop-exec-status-' + exec.status]">
|
|
1839
|
+
<template v-if="exec.status === 'running'">\u{21BB}</template>
|
|
1840
|
+
<template v-else-if="exec.status === 'success'">\u{2713}</template>
|
|
1841
|
+
<template v-else-if="exec.status === 'error'">\u{2717}</template>
|
|
1842
|
+
<template v-else-if="exec.status === 'cancelled'">\u{25CB}</template>
|
|
1843
|
+
<template v-else>?</template>
|
|
1844
|
+
</span>
|
|
1845
|
+
<span class="loop-exec-time">{{ formatExecTime(exec.startedAt) }}</span>
|
|
1846
|
+
<span v-if="exec.status === 'running'" class="loop-exec-running-label">Running...</span>
|
|
1847
|
+
<span v-else-if="exec.durationMs" class="loop-exec-duration">{{ formatDuration(exec.durationMs) }}</span>
|
|
1848
|
+
<span v-if="exec.error" class="loop-exec-error-text" :title="exec.error">{{ exec.error.length > 40 ? exec.error.slice(0, 40) + '...' : exec.error }}</span>
|
|
1849
|
+
<span v-if="exec.trigger === 'manual'" class="loop-exec-trigger-badge">manual</span>
|
|
1850
|
+
</div>
|
|
1851
|
+
<div class="loop-exec-item-right">
|
|
1852
|
+
<button v-if="exec.status === 'running'" class="loop-action-btn" @click="viewExecution(selectedLoop.id, exec.id)">View</button>
|
|
1853
|
+
<button v-if="exec.status === 'running'" class="loop-action-btn loop-action-cancel" @click="cancelLoopExecution(selectedLoop.id)">Cancel</button>
|
|
1854
|
+
<button v-if="exec.status !== 'running'" class="loop-action-btn" @click="viewExecution(selectedLoop.id, exec.id)">View</button>
|
|
1855
|
+
</div>
|
|
1856
|
+
</div>
|
|
1857
|
+
<!-- Load more executions -->
|
|
1858
|
+
<div v-if="hasMoreExecutions && !loadingExecutions" class="loop-load-more">
|
|
1859
|
+
<button class="loop-action-btn" :disabled="loadingMoreExecutions" @click="loadMoreExecutions()">
|
|
1860
|
+
{{ loadingMoreExecutions ? 'Loading...' : 'Load more' }}
|
|
1861
|
+
</button>
|
|
1862
|
+
</div>
|
|
1863
|
+
</div>
|
|
1864
|
+
</div>
|
|
1865
|
+
</div>
|
|
1866
|
+
</div>
|
|
1867
|
+
|
|
1868
|
+
<!-- ── Loop creation panel (default) ── -->
|
|
1869
|
+
<div v-else class="team-create-panel">
|
|
1870
|
+
<div class="team-create-inner">
|
|
1871
|
+
<div class="team-create-header">
|
|
1872
|
+
<svg viewBox="0 0 24 24" width="28" height="28"><path fill="currentColor" opacity="0.5" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
|
|
1873
|
+
<h2>{{ editingLoopId ? 'Edit Loop' : 'Create a Scheduled Loop' }}</h2>
|
|
1874
|
+
</div>
|
|
1875
|
+
<p class="team-create-desc">Configure recurring tasks that run on a schedule. Select a template or create your own.</p>
|
|
1876
|
+
|
|
1877
|
+
<!-- Template cards -->
|
|
1878
|
+
<div v-if="!editingLoopId" class="team-examples-section" style="margin-top: 0;">
|
|
1879
|
+
<div class="team-examples-header">Templates</div>
|
|
1880
|
+
<div class="team-examples-list">
|
|
1881
|
+
<div v-for="key in LOOP_TEMPLATE_KEYS" :key="key"
|
|
1882
|
+
:class="['team-example-card', { 'loop-template-selected': loopSelectedTemplate === key }]"
|
|
1883
|
+
>
|
|
1884
|
+
<div class="team-example-icon">
|
|
1885
|
+
<svg v-if="key === 'competitive-intel'" viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95a15.65 15.65 0 0 0-1.38-3.56A8.03 8.03 0 0 1 18.92 8zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56A7.987 7.987 0 0 1 5.08 16zm2.95-8H5.08a7.987 7.987 0 0 1 4.33-3.56A15.65 15.65 0 0 0 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2s.07-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/></svg>
|
|
1886
|
+
<svg v-else-if="key === 'knowledge-base'" viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
|
1887
|
+
<svg v-else viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
|
|
1888
|
+
</div>
|
|
1889
|
+
<div class="team-example-body">
|
|
1890
|
+
<div class="team-example-title">{{ LOOP_TEMPLATES[key].label }}</div>
|
|
1891
|
+
<div class="team-example-text">{{ LOOP_TEMPLATES[key].description }}</div>
|
|
1892
|
+
</div>
|
|
1893
|
+
<button class="team-example-try" @click="selectLoopTemplate(key)">Try it</button>
|
|
1894
|
+
</div>
|
|
1895
|
+
</div>
|
|
1896
|
+
</div>
|
|
1897
|
+
|
|
1898
|
+
<!-- Name field -->
|
|
1899
|
+
<div class="team-tpl-section">
|
|
1900
|
+
<label class="team-tpl-label">Name</label>
|
|
1901
|
+
<input
|
|
1902
|
+
v-model="loopName"
|
|
1903
|
+
type="text"
|
|
1904
|
+
class="loop-name-input"
|
|
1905
|
+
placeholder="e.g. Daily Code Review"
|
|
1906
|
+
/>
|
|
1907
|
+
</div>
|
|
1908
|
+
|
|
1909
|
+
<!-- Prompt field -->
|
|
1910
|
+
<div class="team-tpl-section">
|
|
1911
|
+
<label class="team-tpl-label">Prompt</label>
|
|
1912
|
+
<textarea
|
|
1913
|
+
v-model="loopPrompt"
|
|
1914
|
+
class="team-create-textarea"
|
|
1915
|
+
placeholder="Describe what the Loop should do each time it runs..."
|
|
1916
|
+
rows="5"
|
|
1917
|
+
></textarea>
|
|
1918
|
+
</div>
|
|
1919
|
+
|
|
1920
|
+
<!-- Schedule selector -->
|
|
1921
|
+
<div class="team-tpl-section">
|
|
1922
|
+
<label class="team-tpl-label">Schedule</label>
|
|
1923
|
+
<div class="loop-schedule-options">
|
|
1924
|
+
<label class="loop-schedule-radio">
|
|
1925
|
+
<input type="radio" v-model="loopScheduleType" value="manual" />
|
|
1926
|
+
<span>Manual</span>
|
|
1927
|
+
<span v-if="loopScheduleType === 'manual'" class="loop-schedule-detail" style="opacity:0.6">run only when triggered</span>
|
|
1928
|
+
</label>
|
|
1929
|
+
<label class="loop-schedule-radio">
|
|
1930
|
+
<input type="radio" v-model="loopScheduleType" value="hourly" />
|
|
1931
|
+
<span>Every hour</span>
|
|
1932
|
+
<span v-if="loopScheduleType === 'hourly'" class="loop-schedule-detail">at minute {{ padTwo(loopScheduleMinute) }}</span>
|
|
1933
|
+
</label>
|
|
1934
|
+
<label class="loop-schedule-radio">
|
|
1935
|
+
<input type="radio" v-model="loopScheduleType" value="daily" />
|
|
1936
|
+
<span>Every day</span>
|
|
1937
|
+
<span v-if="loopScheduleType === 'daily'" class="loop-schedule-detail">
|
|
1938
|
+
at
|
|
1939
|
+
<input type="number" v-model.number="loopScheduleHour" min="0" max="23" class="loop-time-input" />
|
|
1940
|
+
:
|
|
1941
|
+
<input type="number" v-model.number="loopScheduleMinute" min="0" max="59" class="loop-time-input" />
|
|
1942
|
+
</span>
|
|
1943
|
+
</label>
|
|
1944
|
+
<label class="loop-schedule-radio">
|
|
1945
|
+
<input type="radio" v-model="loopScheduleType" value="cron" />
|
|
1946
|
+
<span>Advanced (cron)</span>
|
|
1947
|
+
<span v-if="loopScheduleType === 'cron'" class="loop-schedule-detail">
|
|
1948
|
+
<input type="text" v-model="loopCronExpr" class="loop-cron-input" placeholder="0 9 * * *" />
|
|
1949
|
+
</span>
|
|
1950
|
+
</label>
|
|
1951
|
+
</div>
|
|
1952
|
+
</div>
|
|
1953
|
+
|
|
1954
|
+
<!-- Action buttons -->
|
|
1955
|
+
<div class="team-create-actions">
|
|
1956
|
+
<button v-if="editingLoopId" class="team-create-launch" :disabled="!loopName.trim() || !loopPrompt.trim()" @click="saveLoopEdits()">
|
|
1957
|
+
Save Changes
|
|
1958
|
+
</button>
|
|
1959
|
+
<button v-else class="team-create-launch" :disabled="!loopName.trim() || !loopPrompt.trim()" @click="createLoopFromPanel()">
|
|
1960
|
+
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
|
|
1961
|
+
Create Loop
|
|
1962
|
+
</button>
|
|
1963
|
+
<button v-if="editingLoopId" class="team-create-cancel" @click="cancelEditingLoop()">Cancel</button>
|
|
1964
|
+
<button class="team-create-cancel" @click="backToChat()">Back to Chat</button>
|
|
1965
|
+
</div>
|
|
1966
|
+
|
|
1967
|
+
<!-- Error message -->
|
|
1968
|
+
<div v-if="loopError" class="loop-error-banner" @click="clearLoopError()">
|
|
1969
|
+
<span class="loop-error-icon">\u{26A0}</span>
|
|
1970
|
+
<span class="loop-error-text">{{ loopError }}</span>
|
|
1971
|
+
<span class="loop-error-dismiss">\u{2715}</span>
|
|
1972
|
+
</div>
|
|
1973
|
+
|
|
1974
|
+
<!-- Active Loops list -->
|
|
1975
|
+
<div v-if="loopsList.length > 0" class="loop-active-section">
|
|
1976
|
+
<div class="loop-active-header">Active Loops</div>
|
|
1977
|
+
<div class="loop-active-list">
|
|
1978
|
+
<div v-for="l in loopsList" :key="l.id" class="loop-active-item">
|
|
1979
|
+
<div class="loop-active-item-info" @click="viewLoop(l.id)">
|
|
1980
|
+
<div class="loop-active-item-top">
|
|
1981
|
+
<span class="loop-active-item-name">{{ l.name }}</span>
|
|
1982
|
+
<span :class="['loop-status-dot', l.enabled ? 'loop-status-dot-on' : 'loop-status-dot-off']"></span>
|
|
1983
|
+
</div>
|
|
1984
|
+
<div class="loop-active-item-meta">
|
|
1985
|
+
<span class="loop-active-item-schedule">{{ loopScheduleDisplay(l) }}</span>
|
|
1986
|
+
<span v-if="l.lastExecution" class="loop-active-item-last">
|
|
1987
|
+
Last: {{ loopLastRunDisplay(l) }}
|
|
1988
|
+
</span>
|
|
1989
|
+
<span v-if="isLoopRunning(l.id)" class="loop-exec-running-label">Running...</span>
|
|
1990
|
+
</div>
|
|
1991
|
+
</div>
|
|
1992
|
+
<div class="loop-active-item-actions">
|
|
1993
|
+
<button class="loop-action-btn loop-action-sm" @click="startEditingLoop(l)" title="Edit">Edit</button>
|
|
1994
|
+
<button class="loop-action-btn loop-action-sm loop-action-run" @click="runNow(l.id)" :disabled="isLoopRunning(l.id)" title="Run now">Run</button>
|
|
1995
|
+
<button class="loop-action-btn loop-action-sm" @click="toggleLoop(l.id)" :title="l.enabled ? 'Disable' : 'Enable'">{{ l.enabled ? 'Pause' : 'Resume' }}</button>
|
|
1996
|
+
<button v-if="!l.enabled" class="loop-action-btn loop-action-sm loop-action-delete" @click="requestDeleteLoop(l)" title="Delete">Del</button>
|
|
1997
|
+
</div>
|
|
1998
|
+
</div>
|
|
1999
|
+
</div>
|
|
2000
|
+
</div>
|
|
2001
|
+
</div>
|
|
2002
|
+
</div>
|
|
2003
|
+
|
|
2004
|
+
<!-- Running Loop notification banner -->
|
|
2005
|
+
<div v-if="hasRunningLoop && !selectedLoop" class="loop-running-banner">
|
|
2006
|
+
<span class="loop-running-banner-dot"></span>
|
|
2007
|
+
<span>{{ firstRunningLoop.name }} is running...</span>
|
|
2008
|
+
<button class="loop-action-btn loop-action-sm" @click="viewLoop(firstRunningLoop.loopId)">View</button>
|
|
2009
|
+
</div>
|
|
2010
|
+
|
|
2011
|
+
<!-- Loop delete confirm dialog -->
|
|
2012
|
+
<div v-if="loopDeleteConfirmOpen" class="modal-overlay" @click.self="cancelDeleteLoop()">
|
|
2013
|
+
<div class="modal-dialog">
|
|
2014
|
+
<div class="modal-title">Delete Loop</div>
|
|
2015
|
+
<div class="modal-body">Are you sure you want to delete <strong>{{ loopDeleteConfirmName }}</strong>? This cannot be undone.</div>
|
|
2016
|
+
<div class="modal-actions">
|
|
2017
|
+
<button class="modal-confirm-btn" @click="confirmDeleteLoop()">Delete</button>
|
|
2018
|
+
<button class="modal-cancel-btn" @click="cancelDeleteLoop()">Cancel</button>
|
|
2019
|
+
</div>
|
|
2020
|
+
</div>
|
|
2021
|
+
</div>
|
|
2022
|
+
</template>
|
|
2023
|
+
|
|
1428
2024
|
<!-- ══ Normal Chat ══ -->
|
|
1429
|
-
<template v-else>
|
|
2025
|
+
<template v-else-if="viewMode === 'chat'">
|
|
1430
2026
|
<div class="message-list" @scroll="onMessageListScroll">
|
|
1431
2027
|
<div class="message-list-inner">
|
|
1432
2028
|
<div v-if="messages.length === 0 && status === 'Connected' && !loadingHistory" class="empty-state">
|
|
@@ -1598,7 +2194,7 @@ const App = {
|
|
|
1598
2194
|
</template>
|
|
1599
2195
|
|
|
1600
2196
|
<!-- Input area (shown in both chat and team create mode) -->
|
|
1601
|
-
<div class="input-area" v-if="
|
|
2197
|
+
<div class="input-area" v-if="viewMode === 'chat'">
|
|
1602
2198
|
<input
|
|
1603
2199
|
type="file"
|
|
1604
2200
|
ref="fileInputRef"
|