@agent-link/server 0.1.157 → 0.1.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/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.teamMode.value = 'chat';
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
- teamMode: team.teamMode,
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: teamMode === 'chat' }]" @click="teamMode = 'chat'">Chat</button>
729
- <button :class="['team-mode-btn', { active: teamMode === 'team' }]" @click="teamMode = 'team'">Team</button>
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">&#10003;</button>
1193
+ <button class="session-rename-cancel" @click.stop="cancelRename" title="Cancel">&times;</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
- <div class="sidebar-section sidebar-sessions">
951
- <div class="sidebar-section-header" @click="chatsCollapsed = !chatsCollapsed" style="cursor: pointer;">
952
- <span>Chat History</span>
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="requestSessionList" title="Refresh" :disabled="loadingSessions">
955
- <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>
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="chatsCollapsed ? 'Expand' : 'Collapse'">
958
- <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>
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="!chatsCollapsed" class="sidebar-section-collapsible">
964
- <button class="new-conversation-btn" @click="newConversation">
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 conversation
1302
+ New loop
967
1303
  </button>
968
1304
 
969
- <div v-if="loadingSessions && historySessions.length === 0" class="sidebar-loading">
970
- Loading sessions...
1305
+ <div v-if="loopsList.length === 0 && !loadingLoops" class="sidebar-empty">
1306
+ No loops configured.
971
1307
  </div>
972
- <div v-else-if="historySessions.length === 0" class="sidebar-empty">
973
- No previous sessions found.
974
- </div>
975
- <div v-else class="session-list">
976
- <div v-for="group in groupedSessions" :key="group.label" class="session-group">
977
- <div class="session-group-label">{{ group.label }}</div>
978
- <div
979
- v-for="s in group.sessions" :key="s.sessionId"
980
- :class="['session-item', { active: currentClaudeSessionId === s.sessionId, processing: isSessionProcessing(s.sessionId) }]"
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="renameText"
1319
+ v-model="renameLoopText"
989
1320
  @click.stop
990
- @keydown.enter.stop="confirmRename"
991
- @keydown.escape.stop="cancelRename"
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="confirmRename" title="Confirm">&#10003;</button>
995
- <button class="session-rename-cancel" @click.stop="cancelRename" title="Cancel">&times;</button>
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">&#10003;</button>
1326
+ <button class="session-rename-cancel" @click.stop="cancelLoopRename" title="Cancel">&times;</button>
1000
1327
  </div>
1001
- <div class="session-meta">
1002
- <span>{{ formatRelativeTime(s.lastModified) }}</span>
1003
- <span v-if="renamingSessionId !== s.sessionId" class="session-actions">
1004
- <button
1005
- class="session-rename-btn"
1006
- @click.stop="startRename(s)"
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="teamMode === 'team'">
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="teamMode !== 'team'">
2197
+ <div class="input-area" v-if="viewMode === 'chat'">
1602
2198
  <input
1603
2199
  type="file"
1604
2200
  ref="fileInputRef"