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,271 @@
1
+ // Event Stream — migrated to Tab SDK plugin
2
+ // This file replaces the old event-stream.js + HTML template
3
+ import { registerTab } from '/js/ui/tab-sdk.js';
4
+ import { escapeHtml, getToolDetail } from '/js/core/utils.js';
5
+
6
+ registerTab({
7
+ id: 'events',
8
+ title: 'Events',
9
+ icon: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
10
+ lazy: true,
11
+
12
+ init(ctx) {
13
+ let events = [];
14
+ let activeFilter = 'all';
15
+ let searchQuery = '';
16
+ let listEl, countEl, autoscrollEl, searchEl;
17
+
18
+ // ── Build DOM ─────────────────────────────────────
19
+ const root = document.createElement('div');
20
+ root.className = 'event-stream-tab';
21
+ root.style.cssText = 'display:flex;flex-direction:column;flex:1;overflow:hidden;';
22
+
23
+ root.innerHTML = `
24
+ <div class="event-stream-toolbar">
25
+ <div class="event-stream-filters">
26
+ <button class="event-filter-btn active" data-filter="all">All</button>
27
+ <button class="event-filter-btn" data-filter="tool">Tools</button>
28
+ <button class="event-filter-btn" data-filter="error">Errors</button>
29
+ <button class="event-filter-btn" data-filter="result">Results</button>
30
+ </div>
31
+ <input type="text" placeholder="Search events..." autocomplete="off" class="event-stream-search">
32
+ <button class="event-stream-clear-btn" title="Clear">Clear</button>
33
+ </div>
34
+ <div class="event-stream-list">
35
+ <div class="event-stream-empty">
36
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
37
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
38
+ </svg>
39
+ <span>No events yet</span>
40
+ <span class="event-stream-empty-hint">Events will appear here as the AI works</span>
41
+ </div>
42
+ </div>
43
+ <div class="event-stream-footer">
44
+ <span class="event-stream-count">0 events</span>
45
+ <label class="event-autoscroll-toggle" title="Auto-scroll to latest">
46
+ <input type="checkbox" checked> Auto
47
+ </label>
48
+ </div>
49
+ `;
50
+
51
+ listEl = root.querySelector('.event-stream-list');
52
+ countEl = root.querySelector('.event-stream-count');
53
+ autoscrollEl = root.querySelector('.event-autoscroll-toggle input');
54
+ searchEl = root.querySelector('.event-stream-search');
55
+
56
+ // ── Helpers ───────────────────────────────────────
57
+ function formatTime(ts) {
58
+ const d = ts instanceof Date ? ts : new Date(ts);
59
+ return d.toTimeString().slice(0, 8);
60
+ }
61
+
62
+ function badgeClass(type) {
63
+ switch (type) {
64
+ case 'tool': return 'badge-tool';
65
+ case 'result': return 'badge-result';
66
+ case 'error': return 'badge-error';
67
+ case 'done': return 'badge-done';
68
+ default: return 'badge-tool';
69
+ }
70
+ }
71
+
72
+ function badgeLabel(type) {
73
+ switch (type) {
74
+ case 'tool': return 'TOOL';
75
+ case 'result': return 'OK';
76
+ case 'error': return 'ERR';
77
+ case 'done': return 'DONE';
78
+ default: return type.toUpperCase();
79
+ }
80
+ }
81
+
82
+ function renderEvent(evt) {
83
+ const row = document.createElement('div');
84
+ row.className = 'event-row';
85
+ row.dataset.type = evt.type;
86
+
87
+ const content = document.createElement('div');
88
+ content.style.cssText = 'display:flex;align-items:flex-start;gap:8px;flex-grow:1;min-width:0;';
89
+ content.innerHTML = `
90
+ <span class="event-time">${formatTime(evt.timestamp)}</span>
91
+ <span class="event-badge ${badgeClass(evt.type)}">${badgeLabel(evt.type)}</span>
92
+ <span class="event-summary">${escapeHtml(evt.summary)}</span>
93
+ `;
94
+ row.appendChild(content);
95
+
96
+ if (evt.detail) {
97
+ const detail = document.createElement('div');
98
+ detail.className = 'event-detail';
99
+ detail.textContent = evt.detail;
100
+ row.appendChild(detail);
101
+ }
102
+
103
+ row.addEventListener('click', () => row.classList.toggle('expanded'));
104
+ return row;
105
+ }
106
+
107
+ function matchesFilter(evt) {
108
+ if (activeFilter === 'all') return true;
109
+ if (activeFilter === 'tool') return evt.type === 'tool';
110
+ if (activeFilter === 'error') return evt.type === 'error';
111
+ if (activeFilter === 'result') return evt.type === 'result' || evt.type === 'done';
112
+ return true;
113
+ }
114
+
115
+ function matchesSearch(row) {
116
+ if (!searchQuery) return true;
117
+ return row.textContent.toLowerCase().includes(searchQuery.toLowerCase());
118
+ }
119
+
120
+ function applyFilters() {
121
+ let visible = 0;
122
+ for (const row of listEl.children) {
123
+ const show = matchesFilter({ type: row.dataset.type }) && matchesSearch(row);
124
+ row.style.display = show ? '' : 'none';
125
+ if (show) visible++;
126
+ }
127
+ updateCount(visible);
128
+ }
129
+
130
+ function updateCount(count) {
131
+ const n = count != null ? count : events.length;
132
+ countEl.textContent = `${n} event${n !== 1 ? 's' : ''}`;
133
+ ctx.showBadge(events.length);
134
+ }
135
+
136
+ function autoScroll() {
137
+ if (autoscrollEl.checked) {
138
+ listEl.scrollTop = listEl.scrollHeight;
139
+ }
140
+ }
141
+
142
+ function addEvent(evt) {
143
+ evt.timestamp = evt.timestamp || new Date();
144
+ events.push(evt);
145
+ // Remove empty state placeholder if present
146
+ const emptyEl = listEl.querySelector('.event-stream-empty');
147
+ if (emptyEl) emptyEl.remove();
148
+ const row = renderEvent(evt);
149
+ listEl.appendChild(row);
150
+ if (!matchesFilter(evt) || !matchesSearch(row)) {
151
+ row.style.display = 'none';
152
+ }
153
+ updateCount();
154
+ autoScroll();
155
+ }
156
+
157
+ function clearEvents() {
158
+ events = [];
159
+ listEl.innerHTML = `
160
+ <div class="event-stream-empty">
161
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
162
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
163
+ </svg>
164
+ <span>No events yet</span>
165
+ <span class="event-stream-empty-hint">Events will appear here as the AI works</span>
166
+ </div>`;
167
+ updateCount(0);
168
+ }
169
+
170
+ async function loadSessionEvents(sessionId) {
171
+ if (!sessionId) return;
172
+ try {
173
+ const messages = await (await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages-single`)).json();
174
+ for (const msg of messages) {
175
+ const data = JSON.parse(msg.content);
176
+ const ts = msg.created_at ? new Date(msg.created_at * 1000) : new Date();
177
+ switch (msg.role) {
178
+ case 'tool':
179
+ addEvent({
180
+ type: 'tool', timestamp: ts,
181
+ summary: `${data.name}: ${getToolDetail(data.name, data.input) || '(no detail)'}`,
182
+ detail: JSON.stringify(data.input, null, 2),
183
+ toolName: data.name,
184
+ });
185
+ break;
186
+ case 'tool_result':
187
+ addEvent({
188
+ type: data.isError ? 'error' : 'result', timestamp: ts,
189
+ summary: (data.content || '').slice(0, 100),
190
+ detail: data.content,
191
+ });
192
+ break;
193
+ case 'result':
194
+ addEvent({
195
+ type: 'done', timestamp: ts,
196
+ summary: `${data.model || ''} · ${data.num_turns || 0} turns · $${(data.cost_usd || 0).toFixed(4)}`,
197
+ });
198
+ break;
199
+ case 'error':
200
+ addEvent({ type: 'error', timestamp: ts, summary: data.error || 'Unknown error' });
201
+ break;
202
+ }
203
+ }
204
+ } catch (err) {
205
+ console.error('Failed to load session events:', err);
206
+ }
207
+ }
208
+
209
+ // ── UI bindings ──────────────────────────────────
210
+ // Filter buttons
211
+ const filterBtns = root.querySelectorAll('.event-filter-btn');
212
+ for (const btn of filterBtns) {
213
+ btn.addEventListener('click', () => {
214
+ activeFilter = btn.dataset.filter;
215
+ filterBtns.forEach(b => b.classList.toggle('active', b === btn));
216
+ applyFilters();
217
+ });
218
+ }
219
+
220
+ // Search
221
+ let searchTimer = null;
222
+ searchEl.addEventListener('input', () => {
223
+ clearTimeout(searchTimer);
224
+ searchTimer = setTimeout(() => {
225
+ searchQuery = searchEl.value;
226
+ applyFilters();
227
+ }, 200);
228
+ });
229
+
230
+ // Clear
231
+ root.querySelector('.event-stream-clear-btn').addEventListener('click', () => clearEvents());
232
+
233
+ // ── Event listeners ──────────────────────────────
234
+ ctx.on('ws:message', (msg) => {
235
+ switch (msg.type) {
236
+ case 'tool':
237
+ addEvent({
238
+ type: 'tool',
239
+ summary: `${msg.name}: ${getToolDetail(msg.name, msg.input) || '(no detail)'}`,
240
+ detail: JSON.stringify(msg.input, null, 2),
241
+ toolName: msg.name,
242
+ });
243
+ break;
244
+ case 'tool_result':
245
+ addEvent({
246
+ type: msg.isError ? 'error' : 'result',
247
+ summary: (msg.content || '').slice(0, 100),
248
+ detail: msg.content,
249
+ });
250
+ break;
251
+ case 'result':
252
+ addEvent({
253
+ type: 'done',
254
+ summary: `${msg.model || ''} · ${msg.num_turns || 0} turns · $${(msg.cost_usd || 0).toFixed(4)}`,
255
+ });
256
+ break;
257
+ case 'error':
258
+ addEvent({ type: 'error', summary: msg.error || 'Unknown error' });
259
+ break;
260
+ }
261
+ });
262
+
263
+ // Session switch
264
+ ctx.onState('sessionId', (newId) => {
265
+ clearEvents();
266
+ if (newId) loadSessionEvents(newId);
267
+ });
268
+
269
+ return root;
270
+ },
271
+ });
@@ -0,0 +1,345 @@
1
+ /* ── Linear Tab ───────────────────────────────────────── */
2
+
3
+ .linear-tab {
4
+ display: flex;
5
+ flex-direction: column;
6
+ flex: 1;
7
+ overflow: hidden;
8
+ height: 100%;
9
+ }
10
+
11
+ /* Tab header with nav + actions */
12
+ .linear-tab-header {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: space-between;
16
+ padding: 8px 12px;
17
+ border-bottom: 1px solid var(--border);
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ .linear-tab-nav {
22
+ display: flex;
23
+ gap: 2px;
24
+ background: var(--bg-tertiary);
25
+ border-radius: 6px;
26
+ padding: 2px;
27
+ }
28
+
29
+ .linear-nav-btn {
30
+ background: none;
31
+ border: none;
32
+ color: var(--text-secondary);
33
+ font-size: 11px;
34
+ font-weight: 600;
35
+ padding: 4px 12px;
36
+ border-radius: 4px;
37
+ cursor: pointer;
38
+ transition: background 0.15s, color 0.15s;
39
+ }
40
+
41
+ .linear-nav-btn:hover {
42
+ color: var(--text-primary);
43
+ }
44
+
45
+ .linear-nav-btn.active {
46
+ background: var(--bg);
47
+ color: var(--text-primary);
48
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
49
+ }
50
+
51
+ .linear-tab-actions {
52
+ display: flex;
53
+ gap: 4px;
54
+ }
55
+
56
+ .linear-tab-actions button {
57
+ background: none;
58
+ border: none;
59
+ color: var(--text-dim);
60
+ cursor: pointer;
61
+ padding: 2px 4px;
62
+ border-radius: var(--radius);
63
+ font-size: 14px;
64
+ line-height: 1;
65
+ transition: color 0.15s, background 0.15s;
66
+ }
67
+
68
+ .linear-tab-actions button:hover {
69
+ color: var(--text);
70
+ background: var(--bg-tertiary);
71
+ }
72
+
73
+ .linear-create-issue-btn {
74
+ font-size: 16px !important;
75
+ font-weight: 600;
76
+ }
77
+
78
+ .linear-tab-actions button.spinning svg {
79
+ animation: linearSpin 0.8s linear infinite;
80
+ }
81
+
82
+ @keyframes linearSpin {
83
+ from { transform: rotate(0deg); }
84
+ to { transform: rotate(360deg); }
85
+ }
86
+
87
+ /* Views */
88
+ .linear-view {
89
+ flex: 1;
90
+ overflow: hidden;
91
+ display: flex;
92
+ flex-direction: column;
93
+ }
94
+
95
+ /* ── Issues view ─────────────────────────────────────── */
96
+
97
+ .linear-issues {
98
+ flex: 1;
99
+ overflow-y: auto;
100
+ padding: 6px 0;
101
+ }
102
+
103
+ .linear-issues::-webkit-scrollbar { width: 4px; }
104
+ .linear-issues::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
105
+
106
+ .linear-issue {
107
+ display: block;
108
+ padding: 8px 12px;
109
+ cursor: pointer;
110
+ text-decoration: none;
111
+ transition: background 0.1s;
112
+ border-bottom: 1px solid var(--border-subtle);
113
+ }
114
+
115
+ .linear-issue:hover {
116
+ background: var(--bg-tertiary);
117
+ }
118
+
119
+ .linear-issue-top {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ }
124
+
125
+ .linear-issue-priority {
126
+ width: 7px;
127
+ height: 7px;
128
+ border-radius: 50%;
129
+ flex-shrink: 0;
130
+ }
131
+
132
+ .linear-issue-id {
133
+ font-size: 11px;
134
+ color: var(--text-dim);
135
+ font-weight: 600;
136
+ flex-shrink: 0;
137
+ }
138
+
139
+ .linear-issue-title {
140
+ font-size: 12px;
141
+ color: var(--text);
142
+ white-space: nowrap;
143
+ overflow: hidden;
144
+ text-overflow: ellipsis;
145
+ flex: 1;
146
+ }
147
+
148
+ .linear-issue-meta {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 6px;
152
+ margin-top: 3px;
153
+ padding-left: 13px;
154
+ font-size: 10px;
155
+ color: var(--text-dim);
156
+ }
157
+
158
+ .linear-issue-state {
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 3px;
162
+ }
163
+
164
+ .linear-issue-state-dot {
165
+ width: 5px;
166
+ height: 5px;
167
+ border-radius: 50%;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ .linear-issue-due {
172
+ color: var(--warning);
173
+ }
174
+
175
+ .linear-issue-label {
176
+ padding: 0 4px;
177
+ border-radius: 2px;
178
+ font-size: 10px;
179
+ line-height: 1.5;
180
+ }
181
+
182
+ .linear-panel-footer {
183
+ padding: 6px 12px;
184
+ border-top: 1px solid var(--border);
185
+ font-size: 10px;
186
+ color: var(--text-dim);
187
+ text-align: center;
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .linear-empty {
192
+ display: flex;
193
+ flex-direction: column;
194
+ align-items: center;
195
+ justify-content: center;
196
+ padding: 32px 16px;
197
+ color: var(--text-dim);
198
+ font-size: 12px;
199
+ text-align: center;
200
+ gap: 8px;
201
+ flex: 1;
202
+ }
203
+
204
+ .linear-empty .linear-empty-icon {
205
+ font-size: 24px;
206
+ opacity: 0.5;
207
+ }
208
+
209
+ /* ── Settings view ───────────────────────────────────── */
210
+
211
+ .linear-settings-view {
212
+ padding: 16px;
213
+ overflow-y: auto;
214
+ }
215
+
216
+ .ls-form {
217
+ display: flex;
218
+ flex-direction: column;
219
+ gap: 14px;
220
+ }
221
+
222
+ .ls-toggle-row {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: space-between;
226
+ }
227
+
228
+ .ls-switch {
229
+ position: relative;
230
+ display: inline-block;
231
+ width: 36px;
232
+ height: 20px;
233
+ cursor: pointer;
234
+ }
235
+
236
+ .ls-switch input {
237
+ opacity: 0;
238
+ width: 0;
239
+ height: 0;
240
+ }
241
+
242
+ .ls-slider {
243
+ position: absolute;
244
+ inset: 0;
245
+ background: var(--border);
246
+ border-radius: 20px;
247
+ transition: background 0.2s;
248
+ }
249
+
250
+ .ls-slider::before {
251
+ content: "";
252
+ position: absolute;
253
+ width: 14px;
254
+ height: 14px;
255
+ left: 3px;
256
+ bottom: 3px;
257
+ background: #fff;
258
+ border-radius: 50%;
259
+ transition: transform 0.2s;
260
+ }
261
+
262
+ .ls-switch input:checked + .ls-slider {
263
+ background: var(--accent);
264
+ }
265
+
266
+ .ls-switch input:checked + .ls-slider::before {
267
+ transform: translateX(16px);
268
+ }
269
+
270
+ .ls-field {
271
+ display: flex;
272
+ flex-direction: column;
273
+ gap: 4px;
274
+ }
275
+
276
+ .ls-label {
277
+ font-size: 11px;
278
+ font-weight: 600;
279
+ color: var(--text-secondary);
280
+ text-transform: uppercase;
281
+ letter-spacing: 0.5px;
282
+ }
283
+
284
+ .ls-input {
285
+ background: var(--bg-tertiary);
286
+ border: 1px solid var(--border);
287
+ border-radius: 6px;
288
+ padding: 8px 10px;
289
+ font-size: 12px;
290
+ font-family: var(--font-mono, monospace);
291
+ color: var(--text-primary);
292
+ outline: none;
293
+ transition: border-color 0.15s;
294
+ }
295
+
296
+ .ls-input:focus {
297
+ border-color: var(--accent);
298
+ }
299
+
300
+ .ls-input::placeholder {
301
+ color: var(--text-tertiary);
302
+ }
303
+
304
+ .ls-hint {
305
+ font-size: 10px;
306
+ color: var(--text-tertiary);
307
+ }
308
+
309
+ .ls-actions {
310
+ display: flex;
311
+ gap: 8px;
312
+ margin-top: 4px;
313
+ }
314
+
315
+ .ls-btn {
316
+ padding: 7px 16px;
317
+ border-radius: 6px;
318
+ border: none;
319
+ font-size: 12px;
320
+ font-weight: 600;
321
+ cursor: pointer;
322
+ transition: opacity 0.15s;
323
+ background: var(--accent);
324
+ color: #fff;
325
+ }
326
+
327
+ .ls-btn:hover { opacity: 0.85; }
328
+ .ls-btn:disabled { opacity: 0.5; cursor: not-allowed; }
329
+
330
+ .ls-btn-secondary {
331
+ background: var(--bg-tertiary);
332
+ color: var(--text-primary);
333
+ border: 1px solid var(--border);
334
+ }
335
+
336
+ .ls-status {
337
+ font-size: 11px;
338
+ padding: 8px 10px;
339
+ border-radius: 6px;
340
+ transition: opacity 0.2s;
341
+ }
342
+
343
+ .ls-status.hidden { display: none; }
344
+ .ls-success { background: rgba(34,197,94,0.12); color: #22c55e; }
345
+ .ls-error { background: rgba(239,68,68,0.12); color: #ef4444; }