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,176 @@
1
+ // Parallel mode — 2x2 chat panes
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { CHAT_IDS } from '../core/constants.js';
5
+ import { handleAutocompleteKeydown, handleSlashAutocomplete } from './commands.js';
6
+
7
+ // Panes map — chatId -> pane state object
8
+ export const panes = new Map();
9
+
10
+ export function getPane(chatId) {
11
+ if (!getState("parallelMode")) return panes.get(null);
12
+ return panes.get(chatId) || panes.get(null);
13
+ }
14
+
15
+ export function initSinglePane() {
16
+ panes.clear();
17
+ panes.set(null, {
18
+ chatId: null,
19
+ messagesDiv: $.messagesDiv,
20
+ messageInput: $.messageInput,
21
+ sendBtn: $.sendBtn,
22
+ stopBtn: $.stopBtn,
23
+ isStreaming: false,
24
+ currentAssistantMsg: null,
25
+ autocompleteEl: document.getElementById("slash-autocomplete"),
26
+ _autocompleteIndex: -1,
27
+ });
28
+ }
29
+
30
+ // Initialize on load
31
+ initSinglePane();
32
+
33
+ export function createChatPane(chatId, index) {
34
+ // Lazy import to avoid circular dependency at module parse time
35
+ const { sendMessage, stopGeneration } = _getLazyChatFns();
36
+
37
+ const container = document.createElement("div");
38
+ container.className = "chat-pane";
39
+ container.dataset.chatId = chatId;
40
+
41
+ const header = document.createElement("div");
42
+ header.className = "chat-pane-header";
43
+ header.innerHTML = `
44
+ <span class="chat-pane-label">Chat ${index + 1}</span>
45
+ <span class="chat-pane-status">idle</span>
46
+ `;
47
+ container.appendChild(header);
48
+
49
+ const msgs = document.createElement("div");
50
+ msgs.className = "messages";
51
+ container.appendChild(msgs);
52
+
53
+ const inputBar = document.createElement("div");
54
+ inputBar.className = "input-bar";
55
+
56
+ const textarea = document.createElement("textarea");
57
+ textarea.placeholder = `Ask Claude... (Chat ${index + 1})`;
58
+ textarea.rows = 1;
59
+ inputBar.appendChild(textarea);
60
+
61
+ const paneSendBtn = document.createElement("button");
62
+ paneSendBtn.className = "pane-send-btn";
63
+ paneSendBtn.title = "Send";
64
+ paneSendBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none"><path d="M3 10l7-7m0 0l7 7m-7-7v14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="rotate(90, 10, 10)"/></svg>`;
65
+ inputBar.appendChild(paneSendBtn);
66
+
67
+ const paneStopBtn = document.createElement("button");
68
+ paneStopBtn.className = "pane-stop-btn hidden";
69
+ paneStopBtn.title = "Stop";
70
+ paneStopBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none"><rect x="4" y="4" width="12" height="12" rx="2" fill="currentColor"/></svg>`;
71
+ inputBar.appendChild(paneStopBtn);
72
+
73
+ const paneAutocomplete = document.createElement("div");
74
+ paneAutocomplete.className = "slash-autocomplete hidden";
75
+ inputBar.appendChild(paneAutocomplete);
76
+
77
+ container.appendChild(inputBar);
78
+
79
+ const state = {
80
+ chatId,
81
+ messagesDiv: msgs,
82
+ messageInput: textarea,
83
+ sendBtn: paneSendBtn,
84
+ stopBtn: paneStopBtn,
85
+ isStreaming: false,
86
+ currentAssistantMsg: null,
87
+ statusEl: header.querySelector(".chat-pane-status"),
88
+ autocompleteEl: paneAutocomplete,
89
+ _autocompleteIndex: -1,
90
+ };
91
+
92
+ paneSendBtn.addEventListener("click", () => sendMessage(state));
93
+ paneStopBtn.addEventListener("click", () => stopGeneration(state));
94
+
95
+ textarea.addEventListener("keydown", (e) => {
96
+ if (handleAutocompleteKeydown(e, state)) return;
97
+ if (e.key === "Enter" && !e.shiftKey) {
98
+ e.preventDefault();
99
+ sendMessage(state);
100
+ }
101
+ });
102
+
103
+ textarea.addEventListener("input", () => {
104
+ textarea.style.height = "auto";
105
+ textarea.style.height = Math.min(textarea.scrollHeight, 80) + "px";
106
+ handleSlashAutocomplete(state);
107
+ });
108
+
109
+ return { container, state };
110
+ }
111
+
112
+ export function enterParallelMode() {
113
+ setState("parallelMode", true);
114
+ $.toggleParallelBtn.checked = true;
115
+
116
+ const chatArea = document.querySelector(".chat-area");
117
+ setState("savedChatArea", chatArea);
118
+
119
+ const grid = document.createElement("div");
120
+ grid.className = "chat-grid";
121
+ grid.id = "chat-grid";
122
+
123
+ panes.clear();
124
+
125
+ for (let i = 0; i < CHAT_IDS.length; i++) {
126
+ const { container, state } = createChatPane(CHAT_IDS[i], i);
127
+ grid.appendChild(container);
128
+ panes.set(CHAT_IDS[i], state);
129
+ }
130
+
131
+ chatArea.replaceWith(grid);
132
+
133
+ const sessionId = getState("sessionId");
134
+ if (sessionId) {
135
+ // Lazy import to avoid circular dependency
136
+ import('../features/sessions.js').then(({ loadPaneMessages }) => {
137
+ for (const chatId of CHAT_IDS) {
138
+ loadPaneMessages(sessionId, chatId);
139
+ }
140
+ });
141
+ }
142
+ }
143
+
144
+ export function exitParallelMode() {
145
+ setState("parallelMode", false);
146
+ $.toggleParallelBtn.checked = false;
147
+
148
+ const grid = document.getElementById("chat-grid");
149
+ const savedChatArea = getState("savedChatArea");
150
+ if (grid && savedChatArea) {
151
+ grid.replaceWith(savedChatArea);
152
+ }
153
+
154
+ initSinglePane();
155
+
156
+ const sessionId = getState("sessionId");
157
+ if (sessionId) {
158
+ import('../features/sessions.js').then(({ loadMessages }) => {
159
+ loadMessages(sessionId);
160
+ });
161
+ }
162
+ }
163
+
164
+ // Lazy getter for chat.js functions to avoid circular dependency
165
+ let _chatFns = null;
166
+ function _getLazyChatFns() {
167
+ if (!_chatFns) {
168
+ // These are set by chat.js during init
169
+ _chatFns = { sendMessage: () => {}, stopGeneration: () => {} };
170
+ }
171
+ return _chatFns;
172
+ }
173
+
174
+ export function _setChatFns(fns) {
175
+ _chatFns = fns;
176
+ }
@@ -0,0 +1,168 @@
1
+ // Permission / Tool Approval system
2
+ import { $ } from '../core/dom.js';
3
+ import { getState } from '../core/store.js';
4
+ import { on as onEvent } from '../core/events.js';
5
+ import { sendNotification } from './notifications.js';
6
+
7
+ const STORAGE_KEY = 'claudeck-perm-mode';
8
+
9
+ // Per-session always-allow set (cleared on refresh or /new)
10
+ const alwaysAllowTools = new Set();
11
+
12
+ // Permission request queue — one modal at a time
13
+ const permissionQueue = [];
14
+ let activeRequest = null;
15
+
16
+ export function getPermissionMode() {
17
+ return $.permModeSelect?.value || 'confirmDangerous';
18
+ }
19
+
20
+ export function clearSessionPermissions() {
21
+ alwaysAllowTools.clear();
22
+ }
23
+
24
+ export function enqueuePermissionRequest(msg) {
25
+ // Check if tool is in always-allow set — auto-approve without modal
26
+ if (alwaysAllowTools.has(msg.toolName)) {
27
+ sendPermissionResponse(msg.id, 'allow');
28
+ return;
29
+ }
30
+
31
+ permissionQueue.push(msg);
32
+ if (!activeRequest) {
33
+ processNext();
34
+ }
35
+ }
36
+
37
+ function processNext() {
38
+ if (permissionQueue.length === 0) {
39
+ activeRequest = null;
40
+ return;
41
+ }
42
+ activeRequest = permissionQueue.shift();
43
+ showPermissionModal(activeRequest);
44
+ }
45
+
46
+ function showPermissionModal(req) {
47
+ $.permModalToolName.textContent = `Tool Approval: ${req.toolName}`;
48
+ $.permModalSummary.textContent = getToolSummary(req.toolName, req.input);
49
+ $.permModalInput.textContent = JSON.stringify(req.input, null, 2);
50
+ $.permAlwaysAllowTool.textContent = req.toolName;
51
+ $.permAlwaysAllowCb.checked = false;
52
+
53
+ // Show background session badge if request is from a bg session
54
+ const bgBadge = document.getElementById('perm-bg-badge');
55
+ if (bgBadge) {
56
+ if (req._bgSessionTitle) {
57
+ bgBadge.textContent = `bg: ${req._bgSessionTitle}`;
58
+ bgBadge.classList.remove('hidden');
59
+ } else {
60
+ bgBadge.classList.add('hidden');
61
+ }
62
+ }
63
+
64
+ $.permModal.classList.remove('hidden');
65
+ $.permAllowBtn.focus();
66
+
67
+ // Notify when tab is not focused
68
+ const bgLabel = req._bgSessionTitle ? ` (${req._bgSessionTitle})` : '';
69
+ sendNotification('Tool Approval Needed', `${req.toolName}${bgLabel}`, `perm-${req.id}`);
70
+ }
71
+
72
+ function getToolSummary(name, input) {
73
+ if (!input) return name;
74
+ if (input.file_path) return input.file_path;
75
+ if (input.command) return input.command.slice(0, 120);
76
+ if (input.pattern) return input.pattern;
77
+ if (input.query) return input.query.slice(0, 120);
78
+ if (input.prompt) return input.prompt.slice(0, 120);
79
+ if (input.url) return input.url;
80
+ return name;
81
+ }
82
+
83
+ function hideModal() {
84
+ $.permModal.classList.add('hidden');
85
+ }
86
+
87
+ function handleAllow() {
88
+ if (!activeRequest) return;
89
+ const id = activeRequest.id;
90
+ const toolName = activeRequest.toolName;
91
+ if ($.permAlwaysAllowCb.checked) {
92
+ alwaysAllowTools.add(toolName);
93
+ }
94
+ hideModal();
95
+ sendPermissionResponse(id, 'allow');
96
+ activeRequest = null;
97
+ processNext();
98
+ }
99
+
100
+ function handleDeny() {
101
+ if (!activeRequest) return;
102
+ const id = activeRequest.id;
103
+ hideModal();
104
+ sendPermissionResponse(id, 'deny');
105
+ activeRequest = null;
106
+ processNext();
107
+ }
108
+
109
+ function sendPermissionResponse(id, behavior) {
110
+ const ws = getState('ws');
111
+ if (ws && ws.readyState === WebSocket.OPEN) {
112
+ ws.send(JSON.stringify({ type: 'permission_response', id, behavior }));
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Handle external permission response (e.g., from Telegram).
118
+ * Auto-dismisses the modal if it's showing the same approval.
119
+ */
120
+ export function handleExternalPermissionResponse(id, behavior) {
121
+ // If the active modal is for this approval, dismiss it
122
+ if (activeRequest && activeRequest.id === id) {
123
+ hideModal();
124
+ activeRequest = null;
125
+ processNext();
126
+ return;
127
+ }
128
+
129
+ // If it's in the queue, remove it
130
+ const idx = permissionQueue.findIndex((r) => r.id === id);
131
+ if (idx !== -1) {
132
+ permissionQueue.splice(idx, 1);
133
+ }
134
+ }
135
+
136
+ // Block Escape from closing this modal (capture phase, before shortcuts.js closeAllModals)
137
+ document.addEventListener('keydown', (e) => {
138
+ if (e.key === 'Escape' && activeRequest && !$.permModal.classList.contains('hidden')) {
139
+ e.stopImmediatePropagation();
140
+ }
141
+ }, true);
142
+
143
+ // Clean up on disconnect
144
+ onEvent('ws:disconnected', () => {
145
+ hideModal();
146
+ permissionQueue.length = 0;
147
+ activeRequest = null;
148
+ });
149
+
150
+ // ── Init ──
151
+ function initPermissions() {
152
+ // Restore saved mode
153
+ const saved = localStorage.getItem(STORAGE_KEY);
154
+ if (saved && $.permModeSelect) {
155
+ $.permModeSelect.value = saved;
156
+ }
157
+
158
+ // Persist mode changes
159
+ $.permModeSelect?.addEventListener('change', () => {
160
+ localStorage.setItem(STORAGE_KEY, $.permModeSelect.value);
161
+ });
162
+
163
+ // Button handlers
164
+ $.permAllowBtn?.addEventListener('click', handleAllow);
165
+ $.permDenyBtn?.addEventListener('click', handleDeny);
166
+ }
167
+
168
+ initPermissions();
@@ -0,0 +1,173 @@
1
+ // Right Panel — tabbed container for Tasks, Files, Git, and plugin tabs
2
+ import { $ } from "../core/dom.js";
3
+ import { emit, on } from "../core/events.js";
4
+ import { initTabSDK } from "./tab-sdk.js";
5
+
6
+ const STORAGE_KEY = "claudeck-right-panel";
7
+ const TAB_KEY = "claudeck-right-panel-tab";
8
+ const WIDTH_KEY = "claudeck-right-panel-width";
9
+ const OLD_LINEAR_KEY = "claudeck-linear-panel";
10
+ const MIN_WIDTH = 200;
11
+ const MAX_WIDTH_RATIO = 0.6; // 60vw
12
+
13
+ function isPanelOpen() {
14
+ return !$.rightPanel.classList.contains("hidden");
15
+ }
16
+
17
+ function getActiveTab() {
18
+ const btn = $.rightPanel.querySelector(".right-panel-tab.active");
19
+ return btn ? btn.dataset.tab : TABS[0];
20
+ }
21
+
22
+ function isValidTab(tabName) {
23
+ return !!$.rightPanel.querySelector(`.right-panel-tab[data-tab="${tabName}"]`);
24
+ }
25
+
26
+ export function openRightPanel(tabName) {
27
+ if (tabName && isValidTab(tabName)) {
28
+ switchTab(tabName);
29
+ }
30
+ $.rightPanel.classList.remove("hidden");
31
+ $.rightPanelToggleBtn.classList.add("active");
32
+ localStorage.setItem(STORAGE_KEY, "open");
33
+ emit("rightPanel:opened", getActiveTab());
34
+ }
35
+
36
+ export function closeRightPanel() {
37
+ $.rightPanel.classList.add("hidden");
38
+ $.rightPanelToggleBtn.classList.remove("active");
39
+ localStorage.setItem(STORAGE_KEY, "closed");
40
+ }
41
+
42
+ export function toggleRightPanel(tabName) {
43
+ if (isPanelOpen()) {
44
+ // If clicking same tab, close. If different tab, switch.
45
+ if (tabName && tabName !== getActiveTab()) {
46
+ switchTab(tabName);
47
+ emit("rightPanel:tabChanged", tabName);
48
+ } else {
49
+ closeRightPanel();
50
+ }
51
+ } else {
52
+ openRightPanel(tabName);
53
+ }
54
+ }
55
+
56
+ function switchTab(tabName) {
57
+ if (!isValidTab(tabName)) return;
58
+ applyTab(tabName);
59
+ emit("rightPanel:tabChanged", tabName);
60
+ }
61
+
62
+ // Visual-only tab switch (no event emitted) — used during init restore
63
+ function restoreTab(tabName) {
64
+ if (!isValidTab(tabName)) return;
65
+ applyTab(tabName);
66
+ }
67
+
68
+ function applyTab(tabName) {
69
+ // Update tab buttons
70
+ $.rightPanel.querySelectorAll(".right-panel-tab").forEach((btn) => {
71
+ btn.classList.toggle("active", btn.dataset.tab === tabName);
72
+ });
73
+
74
+ // Update panes
75
+ $.rightPanel.querySelectorAll(".right-panel-pane").forEach((pane) => {
76
+ pane.classList.toggle("active", pane.dataset.tab === tabName);
77
+ });
78
+
79
+ localStorage.setItem(TAB_KEY, tabName);
80
+ }
81
+
82
+ function initRightPanel() {
83
+ // Migrate old linear panel localStorage key
84
+ const oldState = localStorage.getItem(OLD_LINEAR_KEY);
85
+ if (oldState && !localStorage.getItem(STORAGE_KEY)) {
86
+ localStorage.setItem(STORAGE_KEY, oldState);
87
+ localStorage.removeItem(OLD_LINEAR_KEY);
88
+ }
89
+
90
+ // Tab click handlers
91
+ $.rightPanel.querySelectorAll(".right-panel-tab").forEach((btn) => {
92
+ btn.addEventListener("click", () => {
93
+ const tab = btn.dataset.tab;
94
+ if (isPanelOpen() && getActiveTab() === tab) return;
95
+ switchTab(tab);
96
+ });
97
+ });
98
+
99
+ // Close button
100
+ $.rightPanelClose.addEventListener("click", () => closeRightPanel());
101
+
102
+ // Header toggle button
103
+ $.rightPanelToggleBtn.addEventListener("click", () => toggleRightPanel());
104
+
105
+ // Initialize Tab SDK — allows plugin tabs to register
106
+ initTabSDK();
107
+
108
+ // Restore saved tab (silently — no event, listeners aren't ready yet)
109
+ const savedTab = localStorage.getItem(TAB_KEY);
110
+ if (savedTab && isValidTab(savedTab)) {
111
+ restoreTab(savedTab);
112
+ }
113
+
114
+ // Restore saved width
115
+ const savedWidth = localStorage.getItem(WIDTH_KEY);
116
+ if (savedWidth) {
117
+ const w = parseInt(savedWidth, 10);
118
+ if (w >= MIN_WIDTH) {
119
+ $.rightPanel.style.width = w + "px";
120
+ }
121
+ }
122
+
123
+ // Restore panel state (silently — defer event so other modules register first)
124
+ const saved = localStorage.getItem(STORAGE_KEY);
125
+ if (saved === "open") {
126
+ $.rightPanel.classList.remove("hidden");
127
+ $.rightPanelToggleBtn.classList.add("active");
128
+ // Emit after all modules have initialized their event listeners
129
+ queueMicrotask(() => emit("rightPanel:opened", getActiveTab()));
130
+ }
131
+
132
+ // ── Resize by dragging left edge ────────────────────
133
+ initResize();
134
+ }
135
+
136
+ function initResize() {
137
+ const handle = document.getElementById("right-panel-resize");
138
+ if (!handle) return;
139
+
140
+ let startX = 0;
141
+ let startWidth = 0;
142
+
143
+ function onMouseDown(e) {
144
+ e.preventDefault();
145
+ startX = e.clientX;
146
+ startWidth = $.rightPanel.offsetWidth;
147
+ handle.classList.add("active");
148
+ $.rightPanel.classList.add("resizing");
149
+ document.addEventListener("mousemove", onMouseMove);
150
+ document.addEventListener("mouseup", onMouseUp);
151
+ }
152
+
153
+ function onMouseMove(e) {
154
+ const maxWidth = window.innerWidth * MAX_WIDTH_RATIO;
155
+ // Dragging left increases width (panel is on the right)
156
+ const delta = startX - e.clientX;
157
+ const newWidth = Math.min(Math.max(startWidth + delta, MIN_WIDTH), maxWidth);
158
+ $.rightPanel.style.width = newWidth + "px";
159
+ }
160
+
161
+ function onMouseUp() {
162
+ handle.classList.remove("active");
163
+ $.rightPanel.classList.remove("resizing");
164
+ document.removeEventListener("mousemove", onMouseMove);
165
+ document.removeEventListener("mouseup", onMouseUp);
166
+ // Persist
167
+ localStorage.setItem(WIDTH_KEY, Math.round($.rightPanel.offsetWidth));
168
+ }
169
+
170
+ handle.addEventListener("mousedown", onMouseDown);
171
+ }
172
+
173
+ initRightPanel();
@@ -0,0 +1,143 @@
1
+ // Keyboard shortcuts
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { CHAT_IDS } from '../core/constants.js';
5
+ import { panes } from './parallel.js';
6
+ import { registerCommand } from './commands.js';
7
+ import { toggleRightPanel, openRightPanel } from './right-panel.js';
8
+ import { toggleTipsFeed } from '../panels/tips-feed.js';
9
+
10
+ function closeAllModals() {
11
+ document.querySelectorAll(".modal-overlay:not([data-persistent])").forEach((m) => m.classList.add("hidden"));
12
+ }
13
+
14
+ document.getElementById("shortcuts-modal-close").addEventListener("click", () => {
15
+ $.shortcutsModal.classList.add("hidden");
16
+ });
17
+ $.shortcutsModal.addEventListener("click", (e) => {
18
+ if (e.target === $.shortcutsModal) $.shortcutsModal.classList.add("hidden");
19
+ });
20
+
21
+ document.addEventListener("keydown", (e) => {
22
+ const isMeta = e.metaKey || e.ctrlKey;
23
+
24
+ if (e.key === "Escape") {
25
+ closeAllModals();
26
+ return;
27
+ }
28
+
29
+ const tag = document.activeElement?.tagName;
30
+ if (!isMeta && (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT")) return;
31
+
32
+ if (isMeta && e.key === "k") {
33
+ e.preventDefault();
34
+ $.sessionSearchInput.focus();
35
+ return;
36
+ }
37
+
38
+ if (isMeta && e.key === "n") {
39
+ e.preventDefault();
40
+ $.newSessionBtn.click();
41
+ return;
42
+ }
43
+
44
+ if (isMeta && e.key === "/") {
45
+ e.preventDefault();
46
+ $.shortcutsModal.classList.toggle("hidden");
47
+ return;
48
+ }
49
+
50
+ // Cmd+B — Toggle right panel
51
+ if (isMeta && e.key === "b") {
52
+ e.preventDefault();
53
+ toggleRightPanel();
54
+ return;
55
+ }
56
+
57
+ // Cmd+Shift+E — Open Files tab
58
+ if (isMeta && e.shiftKey && e.key === "E") {
59
+ e.preventDefault();
60
+ openRightPanel("files");
61
+ return;
62
+ }
63
+
64
+ // Cmd+Shift+G — Open Git tab
65
+ if (isMeta && e.shiftKey && e.key === "G") {
66
+ e.preventDefault();
67
+ openRightPanel("git");
68
+ return;
69
+ }
70
+
71
+ // Cmd+Shift+R — Open Repos tab
72
+ if (isMeta && e.shiftKey && e.key === "R") {
73
+ e.preventDefault();
74
+ openRightPanel("repos");
75
+ return;
76
+ }
77
+
78
+ // Cmd+Shift+V — Open Events tab
79
+ if (isMeta && e.shiftKey && e.key === "V") {
80
+ e.preventDefault();
81
+ openRightPanel("events");
82
+ return;
83
+ }
84
+
85
+ // Cmd+Shift+A — Go to Home (Analytics)
86
+ if (isMeta && e.shiftKey && e.key === "A") {
87
+ e.preventDefault();
88
+ setState("view", "home");
89
+ setState("sessionId", null);
90
+ return;
91
+ }
92
+
93
+ // Cmd+Shift+T — Toggle Tips Feed
94
+ if (isMeta && e.shiftKey && e.key === "T") {
95
+ e.preventDefault();
96
+ toggleTipsFeed();
97
+ return;
98
+ }
99
+
100
+ if (isMeta && getState("parallelMode") && e.key >= "1" && e.key <= "4") {
101
+ e.preventDefault();
102
+ const idx = parseInt(e.key) - 1;
103
+ const chatId = CHAT_IDS[idx];
104
+ const pane = panes.get(chatId);
105
+ if (pane && pane.messageInput) {
106
+ pane.messageInput.focus();
107
+ }
108
+ return;
109
+ }
110
+ });
111
+
112
+ registerCommand("shortcuts", {
113
+ category: "app",
114
+ description: "Show keyboard shortcuts",
115
+ execute() {
116
+ $.shortcutsModal.classList.remove("hidden");
117
+ },
118
+ });
119
+
120
+ // Register /files and /git slash commands
121
+ registerCommand("files", {
122
+ category: "app",
123
+ description: "Open file explorer",
124
+ execute() {
125
+ openRightPanel("files");
126
+ },
127
+ });
128
+
129
+ registerCommand("git", {
130
+ category: "app",
131
+ description: "Open git panel",
132
+ execute() {
133
+ openRightPanel("git");
134
+ },
135
+ });
136
+
137
+ registerCommand("events", {
138
+ category: "app",
139
+ description: "Open events panel",
140
+ execute() {
141
+ openRightPanel("events");
142
+ },
143
+ });
@@ -0,0 +1,29 @@
1
+ // Sidebar hamburger toggle for mobile/tablet viewports
2
+ import { $ } from '../core/dom.js';
3
+
4
+ const btn = document.getElementById('sidebar-toggle-btn');
5
+ const backdrop = document.getElementById('sidebar-backdrop');
6
+ const mq = window.matchMedia('(max-width: 1024px)');
7
+
8
+ function openSidebar() { document.body.classList.add('sidebar-open'); }
9
+ function closeSidebar() { document.body.classList.remove('sidebar-open'); }
10
+
11
+ if (btn) btn.addEventListener('click', () => {
12
+ document.body.classList.toggle('sidebar-open');
13
+ });
14
+
15
+ if (backdrop) backdrop.addEventListener('click', closeSidebar);
16
+
17
+ // Close sidebar when a session is selected on mobile
18
+ if ($.sessionList) {
19
+ $.sessionList.addEventListener('click', (e) => {
20
+ if (mq.matches && e.target.closest('li')) {
21
+ closeSidebar();
22
+ }
23
+ });
24
+ }
25
+
26
+ // Close sidebar if viewport grows past tablet breakpoint
27
+ mq.addEventListener('change', (e) => {
28
+ if (!e.matches) closeSidebar();
29
+ });