claudeck 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,228 @@
1
+ // Prompt toolbox + variable templates
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { escapeHtml, slugify } from '../core/utils.js';
5
+ import * as api from '../core/api.js';
6
+ import { commandRegistry, registerCommand } from '../ui/commands.js';
7
+ import { getPane } from '../ui/parallel.js';
8
+
9
+ export function extractVariables(promptText) {
10
+ const matches = promptText.match(/\{\{(\w+)\}\}/g);
11
+ if (!matches) return [];
12
+ return [...new Set(matches.map((m) => m.slice(2, -2)))];
13
+ }
14
+
15
+ export function renderVariablesForm(promptText, variables, onSubmit) {
16
+ const form = document.createElement("div");
17
+ form.className = "prompt-variables-form";
18
+
19
+ const title = document.createElement("h4");
20
+ title.textContent = "Fill in template variables";
21
+ form.appendChild(title);
22
+
23
+ const inputs = {};
24
+ for (const varName of variables) {
25
+ const group = document.createElement("div");
26
+ group.className = "prompt-var-group";
27
+
28
+ const label = document.createElement("label");
29
+ label.textContent = `{{${varName}}}`;
30
+ group.appendChild(label);
31
+
32
+ const input = document.createElement("input");
33
+ input.type = "text";
34
+ input.placeholder = varName;
35
+ input.dataset.varName = varName;
36
+ group.appendChild(input);
37
+
38
+ inputs[varName] = input;
39
+ form.appendChild(group);
40
+ }
41
+
42
+ const sendBtn = document.createElement("button");
43
+ sendBtn.className = "prompt-var-send";
44
+ sendBtn.textContent = "Send";
45
+ sendBtn.addEventListener("click", () => {
46
+ let result = promptText;
47
+ for (const [varName, input] of Object.entries(inputs)) {
48
+ const value = input.value.trim() || varName;
49
+ result = result.replace(new RegExp(`\\{\\{${varName}\\}\\}`, "g"), value);
50
+ }
51
+ form.remove();
52
+ onSubmit(result);
53
+ });
54
+ form.appendChild(sendBtn);
55
+
56
+ const inputEls = Object.values(inputs);
57
+ if (inputEls.length > 0) {
58
+ inputEls[inputEls.length - 1].addEventListener("keydown", (e) => {
59
+ if (e.key === "Enter") {
60
+ e.preventDefault();
61
+ sendBtn.click();
62
+ }
63
+ });
64
+ }
65
+
66
+ return form;
67
+ }
68
+
69
+ export function registerPromptCommands() {
70
+ // Remove old prompt commands
71
+ for (const [name, cmd] of Object.entries(commandRegistry)) {
72
+ if (cmd.category === "prompt") delete commandRegistry[name];
73
+ }
74
+ const prompts = getState("prompts");
75
+ for (const p of prompts) {
76
+ const slug = slugify(p.title);
77
+ if (!slug || commandRegistry[slug]) continue;
78
+ registerCommand(slug, {
79
+ category: "prompt",
80
+ description: p.description,
81
+ execute(args, pane) {
82
+ const variables = extractVariables(p.prompt);
83
+ if (variables.length > 0) {
84
+ const existingForm = document.querySelector(".prompt-variables-form");
85
+ if (existingForm) existingForm.remove();
86
+ const chatArea = document.querySelector(".chat-area");
87
+ if (!chatArea) return;
88
+ const form = renderVariablesForm(p.prompt, variables, (filledPrompt) => {
89
+ pane.messageInput.value = filledPrompt;
90
+ pane.messageInput.style.height = "auto";
91
+ pane.messageInput.style.height = Math.min(pane.messageInput.scrollHeight, 200) + "px";
92
+ import('./chat.js').then(({ sendMessage }) => sendMessage(pane));
93
+ });
94
+ chatArea.appendChild(form);
95
+ const firstInput = form.querySelector("input");
96
+ if (firstInput) firstInput.focus();
97
+ } else {
98
+ pane.messageInput.value = p.prompt;
99
+ pane.messageInput.style.height = "auto";
100
+ pane.messageInput.style.height = Math.min(pane.messageInput.scrollHeight, 200) + "px";
101
+ import('./chat.js').then(({ sendMessage }) => sendMessage(pane));
102
+ }
103
+ },
104
+ });
105
+ }
106
+ }
107
+
108
+ export async function loadPrompts() {
109
+ try {
110
+ const prompts = await api.fetchPrompts();
111
+ setState("prompts", prompts);
112
+ renderToolbox();
113
+ registerPromptCommands();
114
+ } catch (err) {
115
+ console.error("Failed to load prompts:", err);
116
+ }
117
+ }
118
+
119
+ function renderToolbox() {
120
+ const prompts = getState("prompts");
121
+ $.toolboxPanel.innerHTML = "";
122
+ prompts.forEach((p, idx) => {
123
+ const card = document.createElement("div");
124
+ card.className = "toolbox-card";
125
+ card.innerHTML = `
126
+ <button class="toolbox-card-delete" data-idx="${idx}" title="Delete prompt">&times;</button>
127
+ <div class="toolbox-card-title">${escapeHtml(p.title)}</div>
128
+ <div class="toolbox-card-desc">${escapeHtml(p.description)}</div>
129
+ `;
130
+ card.title = p.description;
131
+ card.addEventListener("click", (e) => {
132
+ if (e.target.closest(".toolbox-card-delete")) return;
133
+ const variables = extractVariables(p.prompt);
134
+ $.toolboxPanel.classList.add("hidden");
135
+ $.toolboxBtn.classList.remove("active");
136
+ if (variables.length > 0) {
137
+ const existingForm = document.querySelector(".prompt-variables-form");
138
+ if (existingForm) existingForm.remove();
139
+ const form = renderVariablesForm(p.prompt, variables, (filledPrompt) => {
140
+ $.messageInput.value = filledPrompt;
141
+ $.messageInput.style.height = "auto";
142
+ $.messageInput.style.height = Math.min($.messageInput.scrollHeight, 200) + "px";
143
+ import('./chat.js').then(({ sendMessage }) => sendMessage(getPane(null)));
144
+ });
145
+ $.toolboxPanel.parentElement.appendChild(form);
146
+ const firstInput = form.querySelector("input");
147
+ if (firstInput) firstInput.focus();
148
+ } else {
149
+ $.messageInput.value = p.prompt;
150
+ $.messageInput.style.height = "auto";
151
+ $.messageInput.style.height = Math.min($.messageInput.scrollHeight, 200) + "px";
152
+ $.messageInput.focus();
153
+ }
154
+ });
155
+ card.querySelector(".toolbox-card-delete").addEventListener("click", (e) => {
156
+ e.stopPropagation();
157
+ deletePrompt(idx);
158
+ });
159
+ $.toolboxPanel.appendChild(card);
160
+ });
161
+
162
+ const addCard = document.createElement("div");
163
+ addCard.className = "toolbox-card-add";
164
+ addCard.innerHTML = `+ Add Prompt`;
165
+ addCard.addEventListener("click", () => openPromptModal());
166
+ $.toolboxPanel.appendChild(addCard);
167
+ }
168
+
169
+ function openPromptModal() {
170
+ $.promptForm.reset();
171
+ $.promptModal.classList.remove("hidden");
172
+ document.getElementById("prompt-title").focus();
173
+ }
174
+
175
+ function closePromptModal() {
176
+ $.promptModal.classList.add("hidden");
177
+ }
178
+
179
+ async function savePrompt(title, description, prompt) {
180
+ try {
181
+ await api.createPrompt(title, description, prompt);
182
+ await loadPrompts();
183
+ } catch (err) {
184
+ console.error("Failed to save prompt:", err);
185
+ }
186
+ }
187
+
188
+ async function deletePrompt(idx) {
189
+ try {
190
+ await api.deletePromptApi(idx);
191
+ await loadPrompts();
192
+ } catch (err) {
193
+ console.error("Failed to delete prompt:", err);
194
+ }
195
+ }
196
+
197
+ $.promptForm.addEventListener("submit", async (e) => {
198
+ e.preventDefault();
199
+ const title = document.getElementById("prompt-title").value.trim();
200
+ const description = document.getElementById("prompt-desc").value.trim();
201
+ const prompt = document.getElementById("prompt-text").value.trim();
202
+ if (title && description && prompt) {
203
+ await savePrompt(title, description, prompt);
204
+ closePromptModal();
205
+ }
206
+ });
207
+
208
+ $.modalCloseBtn.addEventListener("click", closePromptModal);
209
+ $.modalCancelBtn.addEventListener("click", closePromptModal);
210
+ $.promptModal.addEventListener("click", (e) => {
211
+ if (e.target === $.promptModal) closePromptModal();
212
+ });
213
+
214
+ // Toolbox toggle
215
+ $.toolboxBtn.addEventListener("click", () => {
216
+ const isOpen = !$.toolboxPanel.classList.contains("hidden");
217
+ if ($.workflowPanel) $.workflowPanel.classList.add("hidden");
218
+ if ($.workflowBtn) $.workflowBtn.classList.remove("active");
219
+ if ($.agentSidebar) { $.agentSidebar.classList.add("hidden"); }
220
+ if ($.agentBtn) { $.agentBtn.classList.remove("active"); }
221
+ if (isOpen) {
222
+ $.toolboxPanel.classList.add("hidden");
223
+ $.toolboxBtn.classList.remove("active");
224
+ } else {
225
+ $.toolboxPanel.classList.remove("hidden");
226
+ $.toolboxBtn.classList.add("active");
227
+ }
228
+ });
@@ -0,0 +1,332 @@
1
+ // Session management
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState, on as onState } from '../core/store.js';
4
+ import { CHAT_IDS } from '../core/constants.js';
5
+ import { escapeHtml } from '../core/utils.js';
6
+ import * as api from '../core/api.js';
7
+ import { panes, enterParallelMode, exitParallelMode } from '../ui/parallel.js';
8
+ import { renderMessagesIntoPane, showWhalyPlaceholder } from '../ui/messages.js';
9
+ import { loadContextGauge } from '../ui/context-gauge.js';
10
+
11
+ const SESSION_STORAGE_KEY = "claudeck-session-id";
12
+
13
+ // Persist sessionId to localStorage whenever it changes
14
+ onState("sessionId", (val) => {
15
+ if (val) {
16
+ localStorage.setItem(SESSION_STORAGE_KEY, val);
17
+ } else {
18
+ localStorage.removeItem(SESSION_STORAGE_KEY);
19
+ }
20
+ });
21
+
22
+ // Restore sessionId from localStorage on module load
23
+ (function restoreSessionId() {
24
+ const saved = localStorage.getItem(SESSION_STORAGE_KEY);
25
+ if (saved && !getState("sessionId")) {
26
+ setState("sessionId", saved);
27
+ }
28
+ })();
29
+
30
+ export async function loadSessions(searchTerm) {
31
+ try {
32
+ const cwd = $.projectSelect.value;
33
+ if (!cwd) {
34
+ renderSessions([]);
35
+ return;
36
+ }
37
+ let sessions;
38
+ if (searchTerm) {
39
+ sessions = await api.searchSessions(searchTerm, cwd);
40
+ } else {
41
+ sessions = await api.fetchSessions(cwd);
42
+ }
43
+ renderSessions(sessions);
44
+ } catch (err) {
45
+ console.error("Failed to load sessions:", err);
46
+ }
47
+ }
48
+
49
+ function renderSessions(sessions) {
50
+ const sessionId = getState("sessionId");
51
+ $.sessionList.innerHTML = "";
52
+
53
+ // Empty state: no project selected
54
+ const cwd = $.projectSelect.value;
55
+ if (!cwd) {
56
+ $.sessionList.innerHTML = `
57
+ <div class="session-empty">
58
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
59
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
60
+ </svg>
61
+ <span>Select a project to view sessions</span>
62
+ </div>`;
63
+ return;
64
+ }
65
+
66
+ // Empty state: no sessions found
67
+ if (sessions.length === 0) {
68
+ const isSearch = $.sessionSearchInput.value.trim();
69
+ $.sessionList.innerHTML = `
70
+ <div class="session-empty">
71
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
72
+ ${isSearch
73
+ ? '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'
74
+ : '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'}
75
+ </svg>
76
+ <span>${isSearch ? 'No matching sessions' : 'No sessions yet'}</span>
77
+ ${!isSearch ? '<span class="session-empty-hint">Start a new conversation to create one</span>' : ''}
78
+ </div>`;
79
+ return;
80
+ }
81
+
82
+ for (const s of sessions) {
83
+ const li = document.createElement("li");
84
+ li.className = s.id === sessionId ? "active" : "";
85
+ const time = s.last_used_at ? new Date(s.last_used_at * 1000).toLocaleString() : "";
86
+ const modeBadge = s.mode === "parallel"
87
+ ? '<span class="session-mode parallel">parallel</span>'
88
+ : s.mode === "both"
89
+ ? '<span class="session-mode both">single + parallel</span>'
90
+ : '<span class="session-mode single">single</span>';
91
+ const displayTitle = s.title || s.project_name || "Session";
92
+ const isPinned = s.pinned === 1;
93
+ const summaryTooltip = s.summary ? escapeHtml(s.summary) : "";
94
+ li.innerHTML = `
95
+ <div class="session-card-header">
96
+ <span class="session-title" title="${escapeHtml(displayTitle)}">${escapeHtml(displayTitle)}</span>
97
+ ${modeBadge}
98
+ <span class="session-card-actions">
99
+ <button class="session-pin${isPinned ? " pinned" : ""}" title="${isPinned ? "Unpin" : "Pin"} session">
100
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="${isPinned ? "currentColor" : "none"}" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
101
+ <path d="M12 17v5"/><path d="M9 2h6l-1 7h4l-5 8h-2l-5-8h4z"/>
102
+ </svg>
103
+ </button>
104
+ <button class="session-delete" title="Delete session">&times;</button>
105
+ </span>
106
+ </div>
107
+ <span class="session-preview">${time}</span>
108
+ `;
109
+ if (summaryTooltip) {
110
+ li.setAttribute("data-summary", summaryTooltip);
111
+ }
112
+ li.querySelector(".session-pin").addEventListener("click", async (e) => {
113
+ e.stopPropagation();
114
+ await api.toggleSessionPin(s.id);
115
+ loadSessions($.sessionSearchInput.value.trim() || undefined);
116
+ });
117
+ li.querySelector(".session-delete").addEventListener("click", (e) => {
118
+ e.stopPropagation();
119
+ deleteSession(s.id);
120
+ });
121
+ const titleSpan = li.querySelector(".session-title");
122
+ titleSpan.addEventListener("dblclick", (e) => {
123
+ e.stopPropagation();
124
+ startInlineEdit(titleSpan, s.id, displayTitle);
125
+ });
126
+ li.addEventListener("click", async (e) => {
127
+ if (e.target.closest(".session-delete") || e.target.closest(".session-pin") || e.target.closest(".session-title-edit")) return;
128
+ const { guardSwitch } = await import('./background-sessions.js');
129
+ guardSwitch(async () => {
130
+ setState("view", "chat");
131
+ setState("sessionId", s.id);
132
+ if (s.project_path) {
133
+ $.projectSelect.value = s.project_path;
134
+ localStorage.setItem("claudeck-cwd", s.project_path);
135
+ }
136
+
137
+ const parallelMode = getState("parallelMode");
138
+ const needsParallel = s.mode === "parallel" || s.mode === "both";
139
+ if (needsParallel && !parallelMode) {
140
+ enterParallelMode();
141
+ } else if (!needsParallel && parallelMode) {
142
+ exitParallelMode();
143
+ }
144
+
145
+ if (getState("parallelMode")) {
146
+ for (const chatId of CHAT_IDS) {
147
+ const pane = panes.get(chatId);
148
+ if (pane) pane.messagesDiv.innerHTML = "";
149
+ }
150
+ } else {
151
+ $.messagesDiv.innerHTML = "";
152
+ }
153
+ await loadMessages(s.id);
154
+ loadSessions();
155
+ });
156
+ });
157
+ // Right-click context menu with dev info
158
+ li.addEventListener("contextmenu", (e) => showSessionContextMenu(e, s));
159
+
160
+ // Show blinking dot for background sessions
161
+ const bgMap = getState("backgroundSessions");
162
+ if (bgMap && bgMap.has(s.id)) {
163
+ const dot = document.createElement("span");
164
+ dot.className = "session-bg-indicator";
165
+ li.querySelector(".session-title").after(dot);
166
+ }
167
+
168
+ $.sessionList.appendChild(li);
169
+ }
170
+ }
171
+
172
+ function startInlineEdit(titleSpan, sessionIdToEdit, currentTitle) {
173
+ const input = document.createElement("input");
174
+ input.type = "text";
175
+ input.className = "session-title-edit";
176
+ input.value = currentTitle;
177
+ titleSpan.replaceWith(input);
178
+ input.focus();
179
+ input.select();
180
+
181
+ async function commitEdit() {
182
+ const newTitle = input.value.trim();
183
+ if (newTitle && newTitle !== currentTitle) {
184
+ await api.updateSessionTitle(sessionIdToEdit, newTitle);
185
+ }
186
+ loadSessions();
187
+ }
188
+
189
+ input.addEventListener("keydown", (e) => {
190
+ if (e.key === "Enter") {
191
+ e.preventDefault();
192
+ commitEdit();
193
+ } else if (e.key === "Escape") {
194
+ loadSessions();
195
+ }
196
+ });
197
+ input.addEventListener("blur", commitEdit);
198
+ }
199
+
200
+ export async function deleteSession(id) {
201
+ try {
202
+ await api.deleteSessionApi(id);
203
+ if (id === getState("sessionId")) {
204
+ setState("sessionId", null);
205
+ if (getState("parallelMode")) {
206
+ for (const chatId of CHAT_IDS) {
207
+ const pane = panes.get(chatId);
208
+ if (pane) {
209
+ pane.messagesDiv.innerHTML = "";
210
+ showWhalyPlaceholder(pane);
211
+ }
212
+ }
213
+ } else {
214
+ $.messagesDiv.innerHTML = "";
215
+ showWhalyPlaceholder();
216
+ }
217
+ }
218
+ await loadSessions();
219
+ } catch (err) {
220
+ console.error("Failed to delete session:", err);
221
+ }
222
+ }
223
+
224
+ export async function loadMessages(sid) {
225
+ if (getState("parallelMode")) {
226
+ for (const chatId of CHAT_IDS) {
227
+ loadPaneMessages(sid, chatId);
228
+ }
229
+ return;
230
+ }
231
+
232
+ const pane = panes.get(null);
233
+ try {
234
+ const messages = await api.fetchSingleMessages(sid);
235
+ renderMessagesIntoPane(messages, pane);
236
+ loadContextGauge(sid);
237
+ } catch (err) {
238
+ console.error("Failed to load messages:", err);
239
+ }
240
+ }
241
+
242
+ export async function loadPaneMessages(sid, chatId) {
243
+ const pane = panes.get(chatId);
244
+ if (!pane) return;
245
+ try {
246
+ let messages = await api.fetchMessagesByChatId(sid, chatId);
247
+
248
+ // For Chat 1 (chat-0): also load single-mode messages as fallback
249
+ if (chatId === CHAT_IDS[0]) {
250
+ const singleMessages = await api.fetchSingleMessages(sid);
251
+ if (singleMessages.length > 0) {
252
+ messages = [...singleMessages, ...messages].sort((a, b) => a.id - b.id);
253
+ }
254
+ }
255
+
256
+ renderMessagesIntoPane(messages, pane);
257
+ } catch (err) {
258
+ console.error(`Failed to load messages for ${chatId}:`, err);
259
+ }
260
+ }
261
+
262
+ // ── Session Context Menu ────────────────────────────────
263
+ let sessionCtxMenu = null;
264
+
265
+ function hideSessionContextMenu() {
266
+ if (sessionCtxMenu) {
267
+ sessionCtxMenu.remove();
268
+ sessionCtxMenu = null;
269
+ }
270
+ }
271
+
272
+ function showSessionContextMenu(e, session) {
273
+ e.preventDefault();
274
+ hideSessionContextMenu();
275
+
276
+ const items = [
277
+ { label: "Copy Session ID", value: session.id },
278
+ { label: "Copy Claude Session ID", value: session.claude_session_id || "(none)" },
279
+ { label: "Copy Project Path", value: session.project_path || "(none)" },
280
+ { label: "Copy Title", value: session.title || session.project_name || "(none)" },
281
+ ];
282
+
283
+ sessionCtxMenu = document.createElement("div");
284
+ sessionCtxMenu.className = "session-ctx-menu";
285
+
286
+ for (const item of items) {
287
+ const btn = document.createElement("button");
288
+ btn.innerHTML = `<span class="ctx-label">${escapeHtml(item.label)}</span><span class="ctx-value">${escapeHtml(String(item.value).slice(0, 40))}</span>`;
289
+ btn.addEventListener("click", () => {
290
+ navigator.clipboard.writeText(item.value);
291
+ btn.querySelector(".ctx-label").textContent = "Copied!";
292
+ setTimeout(hideSessionContextMenu, 400);
293
+ });
294
+ sessionCtxMenu.appendChild(btn);
295
+ }
296
+
297
+ // Generate Summary action
298
+ const summaryBtn = document.createElement("button");
299
+ summaryBtn.innerHTML = `<span class="ctx-label">Generate Summary</span><span class="ctx-value">${session.summary ? "Regenerate" : "No summary yet"}</span>`;
300
+ summaryBtn.addEventListener("click", async () => {
301
+ summaryBtn.querySelector(".ctx-label").textContent = "Generating...";
302
+ const result = await api.generateSummary(session.id);
303
+ hideSessionContextMenu();
304
+ if (result.summary) loadSessions($.sessionSearchInput.value.trim() || undefined);
305
+ });
306
+ sessionCtxMenu.appendChild(summaryBtn);
307
+
308
+ sessionCtxMenu.style.left = e.clientX + "px";
309
+ sessionCtxMenu.style.top = e.clientY + "px";
310
+ document.body.appendChild(sessionCtxMenu);
311
+
312
+ // Keep within viewport
313
+ const rect = sessionCtxMenu.getBoundingClientRect();
314
+ if (rect.right > window.innerWidth) sessionCtxMenu.style.left = (e.clientX - rect.width) + "px";
315
+ if (rect.bottom > window.innerHeight) sessionCtxMenu.style.top = (e.clientY - rect.height) + "px";
316
+ }
317
+
318
+ document.addEventListener("click", (e) => {
319
+ if (sessionCtxMenu && !sessionCtxMenu.contains(e.target)) hideSessionContextMenu();
320
+ });
321
+ document.addEventListener("keydown", (e) => {
322
+ if (e.key === "Escape") hideSessionContextMenu();
323
+ });
324
+
325
+ // Session search with debounce
326
+ let searchDebounceTimer = null;
327
+ $.sessionSearchInput.addEventListener("input", () => {
328
+ clearTimeout(searchDebounceTimer);
329
+ searchDebounceTimer = setTimeout(() => {
330
+ loadSessions($.sessionSearchInput.value.trim());
331
+ }, 200);
332
+ });
@@ -0,0 +1,131 @@
1
+ // Telegram notification settings — UI for configuring bot token, chat ID, and notification preferences
2
+ import { $ } from '../core/dom.js';
3
+ import { registerCommand } from '../ui/commands.js';
4
+
5
+ const NOTIFY_MAP = {
6
+ sessionComplete: 'tgNotifySession',
7
+ workflowComplete: 'tgNotifyWorkflow',
8
+ chainComplete: 'tgNotifyChain',
9
+ agentComplete: 'tgNotifyAgent',
10
+ orchestratorComplete: 'tgNotifyOrchestrator',
11
+ dagComplete: 'tgNotifyDag',
12
+ errors: 'tgNotifyErrors',
13
+ permissionRequests: 'tgNotifyPermissions',
14
+ taskStart: 'tgNotifyStart',
15
+ };
16
+
17
+ async function loadConfig() {
18
+ try {
19
+ const res = await fetch("/api/telegram/config");
20
+ if (!res.ok) return null;
21
+ return await res.json();
22
+ } catch { return null; }
23
+ }
24
+
25
+ function showStatus(msg, isError) {
26
+ $.telegramStatus.textContent = msg;
27
+ $.telegramStatus.className = `telegram-status ${isError ? "error" : "success"}`;
28
+ $.telegramStatus.classList.remove("hidden");
29
+ setTimeout(() => $.telegramStatus.classList.add("hidden"), 4000);
30
+ }
31
+
32
+ function updateLabel(enabled) {
33
+ $.telegramLabel.textContent = enabled ? "Telegram (on)" : "Telegram";
34
+ }
35
+
36
+ async function openModal() {
37
+ const config = await loadConfig();
38
+ if (config) {
39
+ $.telegramEnabled.checked = config.enabled;
40
+ $.telegramBotToken.value = config.botToken || "";
41
+ $.telegramChatId.value = config.chatId || "";
42
+ $.telegramAfkTimeout.value = config.afkTimeoutMinutes || 15;
43
+ updateLabel(config.enabled);
44
+
45
+ // Load notification preferences
46
+ if (config.notify) {
47
+ for (const [key, domKey] of Object.entries(NOTIFY_MAP)) {
48
+ if ($[domKey]) {
49
+ $[domKey].checked = config.notify[key] !== false;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ $.telegramModal.classList.remove("hidden");
55
+ }
56
+
57
+ function closeModal() {
58
+ $.telegramModal.classList.add("hidden");
59
+ }
60
+
61
+ function collectNotifyPrefs() {
62
+ const notify = {};
63
+ for (const [key, domKey] of Object.entries(NOTIFY_MAP)) {
64
+ if ($[domKey]) {
65
+ notify[key] = $[domKey].checked;
66
+ }
67
+ }
68
+ return notify;
69
+ }
70
+
71
+ async function save() {
72
+ const enabled = $.telegramEnabled.checked;
73
+ const botToken = $.telegramBotToken.value.trim();
74
+ const chatId = $.telegramChatId.value.trim();
75
+ const afkTimeoutMinutes = parseInt($.telegramAfkTimeout.value, 10) || 15;
76
+ const notify = collectNotifyPrefs();
77
+
78
+ if (enabled && (!botToken || !chatId)) {
79
+ showStatus("Bot token and chat ID are required", true);
80
+ return;
81
+ }
82
+
83
+ try {
84
+ const res = await fetch("/api/telegram/config", {
85
+ method: "PUT",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({ enabled, botToken, chatId, afkTimeoutMinutes, notify }),
88
+ });
89
+ if (!res.ok) throw new Error((await res.json()).error);
90
+ showStatus("Settings saved", false);
91
+ updateLabel(enabled);
92
+ } catch (err) {
93
+ showStatus(`Save failed: ${err.message}`, true);
94
+ }
95
+ }
96
+
97
+ async function test() {
98
+ $.telegramTestBtn.disabled = true;
99
+ $.telegramTestBtn.textContent = "Sending...";
100
+ try {
101
+ const res = await fetch("/api/telegram/test", { method: "POST" });
102
+ if (!res.ok) throw new Error((await res.json()).error || "Send failed");
103
+ showStatus("Test message sent — check Telegram", false);
104
+ } catch (err) {
105
+ showStatus(`Test failed: ${err.message}`, true);
106
+ } finally {
107
+ $.telegramTestBtn.disabled = false;
108
+ $.telegramTestBtn.textContent = "Send Test";
109
+ }
110
+ }
111
+
112
+ // Wire up
113
+ $.telegramBtn.addEventListener("click", openModal);
114
+ $.telegramClose.addEventListener("click", closeModal);
115
+ $.telegramModal.addEventListener("click", (e) => {
116
+ if (e.target === $.telegramModal) closeModal();
117
+ });
118
+ $.telegramSaveBtn.addEventListener("click", save);
119
+ $.telegramTestBtn.addEventListener("click", test);
120
+
121
+ // Register slash command
122
+ registerCommand("telegram", {
123
+ category: "settings",
124
+ description: "Open Telegram notification settings",
125
+ execute() { openModal(); },
126
+ });
127
+
128
+ // Load initial state
129
+ loadConfig().then(config => {
130
+ if (config) updateLabel(config.enabled);
131
+ });