claudeck 1.4.1 → 1.4.2
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/README.md +2 -4
- package/package.json +1 -1
- package/public/css/core/theme.css +6 -21
- package/public/css/core/variables.css +2 -0
- package/public/css/features/message-queue.css +348 -0
- package/public/css/ui/commands.css +4 -4
- package/public/css/ui/messages.css +310 -78
- package/public/css/ui/sessions.css +173 -0
- package/public/index.html +3 -2
- package/public/js/components/add-project-modal.js +14 -0
- package/public/js/components/jump-to-latest.js +42 -0
- package/public/js/components/queue-stop-modal.js +23 -0
- package/public/js/core/api.js +15 -43
- package/public/js/core/dom.js +17 -0
- package/public/js/core/utils.js +38 -2
- package/public/js/features/chat.js +49 -1
- package/public/js/features/message-queue.js +423 -0
- package/public/js/features/projects.js +185 -3
- package/public/js/main.js +3 -1
- package/public/js/panels/dev-docs.js +1 -1
- package/public/js/ui/formatting.js +65 -11
- package/public/js/ui/messages.js +97 -1
- package/public/js/ui/parallel.js +32 -2
- package/public/js/ui/right-panel.js +0 -8
- package/public/style.css +1 -0
- package/server/routes/projects.js +0 -0
- package/plugins/linear/client.css +0 -345
- package/plugins/linear/client.js +0 -380
- package/plugins/linear/config.json +0 -5
- package/plugins/linear/manifest.json +0 -10
- package/plugins/linear/server.js +0 -312
- package/public/js/components/linear-create-modal.js +0 -43
package/plugins/linear/client.js
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
// Linear Tab — Tab SDK plugin combining Linear issues + settings
|
|
2
|
-
import { registerTab } from '/js/ui/tab-sdk.js';
|
|
3
|
-
|
|
4
|
-
function escapeHtml(str) {
|
|
5
|
-
const div = document.createElement('div');
|
|
6
|
-
div.textContent = str;
|
|
7
|
-
return div.innerHTML;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const ICONS = {
|
|
11
|
-
refresh: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>`,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
registerTab({
|
|
15
|
-
id: 'linear',
|
|
16
|
-
title: 'Linear',
|
|
17
|
-
icon: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
18
|
-
lazy: true,
|
|
19
|
-
|
|
20
|
-
init(ctx) {
|
|
21
|
-
const CACHE_TTL = 60_000;
|
|
22
|
-
let cachedLinear = null;
|
|
23
|
-
let linearCacheTime = 0;
|
|
24
|
-
let linearLoading = false;
|
|
25
|
-
let currentView = 'issues'; // 'issues' | 'settings'
|
|
26
|
-
|
|
27
|
-
// ── Build DOM ──────────────────────────────────────
|
|
28
|
-
const root = document.createElement('div');
|
|
29
|
-
root.className = 'linear-tab';
|
|
30
|
-
|
|
31
|
-
root.innerHTML = `
|
|
32
|
-
<div class="linear-tab-header">
|
|
33
|
-
<div class="linear-tab-nav">
|
|
34
|
-
<button class="linear-nav-btn active" data-view="issues">Issues</button>
|
|
35
|
-
<button class="linear-nav-btn" data-view="settings">Settings</button>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="linear-tab-actions">
|
|
38
|
-
<button class="linear-create-issue-btn" title="Create issue">+</button>
|
|
39
|
-
<button class="linear-refresh-btn" title="Refresh issues">${ICONS.refresh}</button>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<div class="linear-view linear-issues-view">
|
|
44
|
-
<div class="linear-issues"></div>
|
|
45
|
-
<div class="linear-panel-footer"></div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
<div class="linear-view linear-settings-view" style="display:none;">
|
|
49
|
-
<div class="ls-form">
|
|
50
|
-
<div class="ls-toggle-row">
|
|
51
|
-
<span class="ls-label">Enable Integration</span>
|
|
52
|
-
<label class="ls-switch">
|
|
53
|
-
<input type="checkbox" class="ls-enabled" />
|
|
54
|
-
<span class="ls-slider"></span>
|
|
55
|
-
</label>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<label class="ls-field">
|
|
59
|
-
<span class="ls-label">API Key</span>
|
|
60
|
-
<input type="password" class="ls-input ls-api-key" placeholder="lin_api_..." autocomplete="off" spellcheck="false" />
|
|
61
|
-
<span class="ls-hint">Generate at Linear → Settings → API → Personal API keys</span>
|
|
62
|
-
</label>
|
|
63
|
-
|
|
64
|
-
<label class="ls-field">
|
|
65
|
-
<span class="ls-label">Assignee Email</span>
|
|
66
|
-
<input type="email" class="ls-input ls-email" placeholder="you@company.com" autocomplete="off" spellcheck="false" />
|
|
67
|
-
<span class="ls-hint">New issues will be auto-assigned to this user</span>
|
|
68
|
-
</label>
|
|
69
|
-
|
|
70
|
-
<div class="ls-actions">
|
|
71
|
-
<button class="ls-btn ls-save-btn">Save</button>
|
|
72
|
-
<button class="ls-btn ls-test-btn ls-btn-secondary">Test Connection</button>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div class="ls-status hidden"></div>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
`;
|
|
79
|
-
|
|
80
|
-
// ── Selectors ──────────────────────────────────────
|
|
81
|
-
const issuesView = root.querySelector('.linear-issues-view');
|
|
82
|
-
const settingsView = root.querySelector('.linear-settings-view');
|
|
83
|
-
const navBtns = root.querySelectorAll('.linear-nav-btn');
|
|
84
|
-
const actionsBar = root.querySelector('.linear-tab-actions');
|
|
85
|
-
const refreshBtn = root.querySelector('.linear-refresh-btn');
|
|
86
|
-
const createBtn = root.querySelector('.linear-create-issue-btn');
|
|
87
|
-
const issuesList = root.querySelector('.linear-issues');
|
|
88
|
-
const footer = root.querySelector('.linear-panel-footer');
|
|
89
|
-
|
|
90
|
-
// Settings selectors
|
|
91
|
-
const enabledEl = root.querySelector('.ls-enabled');
|
|
92
|
-
const apiKeyEl = root.querySelector('.ls-api-key');
|
|
93
|
-
const emailEl = root.querySelector('.ls-email');
|
|
94
|
-
const saveBtn = root.querySelector('.ls-save-btn');
|
|
95
|
-
const testBtn = root.querySelector('.ls-test-btn');
|
|
96
|
-
const statusEl = root.querySelector('.ls-status');
|
|
97
|
-
|
|
98
|
-
// ── View switching ─────────────────────────────────
|
|
99
|
-
navBtns.forEach(btn => {
|
|
100
|
-
btn.addEventListener('click', () => {
|
|
101
|
-
const view = btn.dataset.view;
|
|
102
|
-
if (view === currentView) return;
|
|
103
|
-
currentView = view;
|
|
104
|
-
navBtns.forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
|
105
|
-
issuesView.style.display = view === 'issues' ? '' : 'none';
|
|
106
|
-
settingsView.style.display = view === 'settings' ? '' : 'none';
|
|
107
|
-
actionsBar.style.display = view === 'issues' ? '' : 'none';
|
|
108
|
-
if (view === 'settings') loadSettings();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// ══════════════════════════════════════════════════
|
|
113
|
-
// ISSUES VIEW
|
|
114
|
-
// ══════════════════════════════════════════════════
|
|
115
|
-
|
|
116
|
-
function priorityColor(priority) {
|
|
117
|
-
switch (priority) {
|
|
118
|
-
case 1: return 'var(--error)';
|
|
119
|
-
case 2: return 'var(--warning)';
|
|
120
|
-
case 3: return 'var(--accent)';
|
|
121
|
-
case 4: return 'var(--text-dim)';
|
|
122
|
-
default: return 'var(--border)';
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatDate(dateStr) {
|
|
127
|
-
if (!dateStr) return null;
|
|
128
|
-
const d = new Date(dateStr);
|
|
129
|
-
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function loadIssues() {
|
|
133
|
-
if (linearLoading) return;
|
|
134
|
-
linearLoading = true;
|
|
135
|
-
refreshBtn.classList.add('spinning');
|
|
136
|
-
issuesList.innerHTML = '<div class="linear-empty"><span class="linear-empty-icon">⌛</span>Loading...</div>';
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const data = await ctx.api.fetchLinearIssues();
|
|
140
|
-
cachedLinear = data;
|
|
141
|
-
linearCacheTime = Date.now();
|
|
142
|
-
|
|
143
|
-
if (data.error && data.issues.length === 0) {
|
|
144
|
-
const isKeyError = data.error.includes('not configured');
|
|
145
|
-
const icon = isKeyError ? '🔑' : '📄';
|
|
146
|
-
const hint = isKeyError
|
|
147
|
-
? '<br><span style="font-size:10px;margin-top:4px;display:block;cursor:pointer;text-decoration:underline;" class="linear-go-settings">Configure in Settings</span>'
|
|
148
|
-
: '';
|
|
149
|
-
issuesList.innerHTML = `<div class="linear-empty"><span class="linear-empty-icon">${icon}</span>${data.error}${hint}</div>`;
|
|
150
|
-
footer.textContent = '';
|
|
151
|
-
|
|
152
|
-
const link = issuesList.querySelector('.linear-go-settings');
|
|
153
|
-
if (link) {
|
|
154
|
-
link.addEventListener('click', () => {
|
|
155
|
-
root.querySelector('.linear-nav-btn[data-view="settings"]').click();
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
renderIssues(data.issues);
|
|
160
|
-
footer.textContent = `\u2500\u2500\u2500 ${data.issues.length} issue${data.issues.length !== 1 ? 's' : ''} \u2500\u2500\u2500`;
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
issuesList.innerHTML = '<div class="linear-empty"><span class="linear-empty-icon">📄</span>Failed to fetch issues</div>';
|
|
164
|
-
footer.textContent = '';
|
|
165
|
-
} finally {
|
|
166
|
-
linearLoading = false;
|
|
167
|
-
refreshBtn.classList.remove('spinning');
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function renderIssues(issues) {
|
|
172
|
-
issuesList.innerHTML = '';
|
|
173
|
-
for (const issue of issues) {
|
|
174
|
-
const a = document.createElement('a');
|
|
175
|
-
a.className = 'linear-issue';
|
|
176
|
-
a.href = issue.url;
|
|
177
|
-
a.target = '_blank';
|
|
178
|
-
a.rel = 'noopener';
|
|
179
|
-
|
|
180
|
-
const due = formatDate(issue.dueDate);
|
|
181
|
-
const labels = (issue.labels?.nodes || [])
|
|
182
|
-
.map(l => `<span class="linear-issue-label" style="background:${l.color}22;color:${l.color}">${l.name}</span>`)
|
|
183
|
-
.join('');
|
|
184
|
-
|
|
185
|
-
a.innerHTML = `
|
|
186
|
-
<div class="linear-issue-top">
|
|
187
|
-
<span class="linear-issue-priority" style="background:${priorityColor(issue.priority)}" title="${issue.priorityLabel}"></span>
|
|
188
|
-
<span class="linear-issue-id">${issue.identifier}</span>
|
|
189
|
-
<span class="linear-issue-title">${escapeHtml(issue.title)}</span>
|
|
190
|
-
</div>
|
|
191
|
-
<div class="linear-issue-meta">
|
|
192
|
-
<span class="linear-issue-state">
|
|
193
|
-
<span class="linear-issue-state-dot" style="background:${issue.state?.color || 'var(--text-dim)'}"></span>
|
|
194
|
-
${escapeHtml(issue.state?.name || '')}
|
|
195
|
-
</span>
|
|
196
|
-
${due ? `<span class="linear-issue-due">Due ${due}</span>` : ''}
|
|
197
|
-
${labels}
|
|
198
|
-
</div>
|
|
199
|
-
`;
|
|
200
|
-
issuesList.appendChild(a);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ── Create Issue Modal (reuse from index.html) ────
|
|
205
|
-
const createModal = document.getElementById('linear-create-modal');
|
|
206
|
-
const createForm = document.getElementById('linear-create-form');
|
|
207
|
-
const createTitle = document.getElementById('linear-create-title');
|
|
208
|
-
const createDesc = document.getElementById('linear-create-desc');
|
|
209
|
-
const createTeam = document.getElementById('linear-create-team');
|
|
210
|
-
const createState = document.getElementById('linear-create-state');
|
|
211
|
-
const createClose = document.getElementById('linear-create-close');
|
|
212
|
-
const createCancel = document.getElementById('linear-create-cancel');
|
|
213
|
-
const createSubmit = document.getElementById('linear-create-submit');
|
|
214
|
-
|
|
215
|
-
function openCreateModal() {
|
|
216
|
-
if (!createModal) return;
|
|
217
|
-
createModal.classList.remove('hidden');
|
|
218
|
-
createForm.reset();
|
|
219
|
-
createState.disabled = true;
|
|
220
|
-
createState.innerHTML = '<option value="">Select a team first...</option>';
|
|
221
|
-
createSubmit.disabled = false;
|
|
222
|
-
createSubmit.textContent = 'Create';
|
|
223
|
-
createTitle.focus();
|
|
224
|
-
|
|
225
|
-
ctx.api.fetchLinearTeams().then((data) => {
|
|
226
|
-
const opts = (data.teams || [])
|
|
227
|
-
.map(t => `<option value="${t.id}">${escapeHtml(t.name)}</option>`)
|
|
228
|
-
.join('');
|
|
229
|
-
createTeam.innerHTML = `<option value="">Select a team...</option>${opts}`;
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function closeCreateModal() {
|
|
234
|
-
if (createModal) createModal.classList.add('hidden');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function handleTeamChange() {
|
|
238
|
-
const teamId = createTeam.value;
|
|
239
|
-
if (!teamId) {
|
|
240
|
-
createState.disabled = true;
|
|
241
|
-
createState.innerHTML = '<option value="">Select a team first...</option>';
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
createState.disabled = true;
|
|
245
|
-
createState.innerHTML = '<option value="">Loading...</option>';
|
|
246
|
-
|
|
247
|
-
ctx.api.fetchLinearTeamStates(teamId).then((data) => {
|
|
248
|
-
const states = data.states || [];
|
|
249
|
-
const opts = states
|
|
250
|
-
.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`)
|
|
251
|
-
.join('');
|
|
252
|
-
createState.innerHTML = `<option value="">Select state...</option>${opts}`;
|
|
253
|
-
createState.disabled = false;
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function handleCreateSubmit(e) {
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
const title = createTitle.value.trim();
|
|
260
|
-
const teamId = createTeam.value;
|
|
261
|
-
if (!title || !teamId) return;
|
|
262
|
-
|
|
263
|
-
createSubmit.disabled = true;
|
|
264
|
-
createSubmit.textContent = 'Creating...';
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const result = await ctx.api.createLinearIssue({
|
|
268
|
-
title,
|
|
269
|
-
description: createDesc.value.trim() || undefined,
|
|
270
|
-
teamId,
|
|
271
|
-
stateId: createState.value || undefined,
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
if (result.success) {
|
|
275
|
-
cachedLinear = null;
|
|
276
|
-
linearCacheTime = 0;
|
|
277
|
-
loadIssues();
|
|
278
|
-
closeCreateModal();
|
|
279
|
-
} else {
|
|
280
|
-
createSubmit.textContent = 'Failed \u2014 retry';
|
|
281
|
-
createSubmit.disabled = false;
|
|
282
|
-
}
|
|
283
|
-
} catch {
|
|
284
|
-
createSubmit.textContent = 'Failed \u2014 retry';
|
|
285
|
-
createSubmit.disabled = false;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
refreshBtn.addEventListener('click', () => loadIssues());
|
|
290
|
-
createBtn.addEventListener('click', () => openCreateModal());
|
|
291
|
-
|
|
292
|
-
if (createClose) createClose.addEventListener('click', closeCreateModal);
|
|
293
|
-
if (createCancel) createCancel.addEventListener('click', closeCreateModal);
|
|
294
|
-
if (createModal) createModal.addEventListener('click', (e) => {
|
|
295
|
-
if (e.target === createModal) closeCreateModal();
|
|
296
|
-
});
|
|
297
|
-
if (createTeam) createTeam.addEventListener('change', handleTeamChange);
|
|
298
|
-
if (createForm) createForm.addEventListener('submit', handleCreateSubmit);
|
|
299
|
-
|
|
300
|
-
// ══════════════════════════════════════════════════
|
|
301
|
-
// SETTINGS VIEW
|
|
302
|
-
// ══════════════════════════════════════════════════
|
|
303
|
-
|
|
304
|
-
function showStatus(msg, isError) {
|
|
305
|
-
statusEl.textContent = msg;
|
|
306
|
-
statusEl.className = `ls-status ${isError ? 'ls-error' : 'ls-success'}`;
|
|
307
|
-
setTimeout(() => { statusEl.className = 'ls-status hidden'; }, 4000);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function loadSettings() {
|
|
311
|
-
try {
|
|
312
|
-
const cfg = await ctx.api.fetchLinearConfig();
|
|
313
|
-
enabledEl.checked = cfg.enabled || false;
|
|
314
|
-
apiKeyEl.value = cfg.apiKey || '';
|
|
315
|
-
emailEl.value = cfg.assigneeEmail || '';
|
|
316
|
-
} catch {
|
|
317
|
-
showStatus('Failed to load config', true);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
saveBtn.addEventListener('click', async () => {
|
|
322
|
-
const enabled = enabledEl.checked;
|
|
323
|
-
const apiKey = apiKeyEl.value.trim();
|
|
324
|
-
const assigneeEmail = emailEl.value.trim();
|
|
325
|
-
|
|
326
|
-
if (enabled && !apiKey) {
|
|
327
|
-
showStatus('API key is required when enabled', true);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
saveBtn.disabled = true;
|
|
332
|
-
saveBtn.textContent = 'Saving...';
|
|
333
|
-
try {
|
|
334
|
-
const res = await ctx.api.saveLinearConfig({ enabled, apiKey, assigneeEmail });
|
|
335
|
-
if (res.error) throw new Error(res.error);
|
|
336
|
-
showStatus('Settings saved', false);
|
|
337
|
-
cachedLinear = null;
|
|
338
|
-
linearCacheTime = 0;
|
|
339
|
-
await loadSettings();
|
|
340
|
-
} catch (err) {
|
|
341
|
-
showStatus(`Save failed: ${err.message}`, true);
|
|
342
|
-
} finally {
|
|
343
|
-
saveBtn.disabled = false;
|
|
344
|
-
saveBtn.textContent = 'Save';
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
testBtn.addEventListener('click', async () => {
|
|
349
|
-
testBtn.disabled = true;
|
|
350
|
-
testBtn.textContent = 'Testing...';
|
|
351
|
-
try {
|
|
352
|
-
const res = await ctx.api.testLinearConnection();
|
|
353
|
-
if (!res.ok) throw new Error(res.error || 'Connection failed');
|
|
354
|
-
showStatus(`Connected as ${res.user?.name || res.user?.email || 'unknown'}`, false);
|
|
355
|
-
} catch (err) {
|
|
356
|
-
showStatus(`Test failed: ${err.message}`, true);
|
|
357
|
-
} finally {
|
|
358
|
-
testBtn.disabled = false;
|
|
359
|
-
testBtn.textContent = 'Test Connection';
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// ── Initial load ───────────────────────────────────
|
|
364
|
-
function loadAll() {
|
|
365
|
-
if (!cachedLinear || Date.now() - linearCacheTime > CACHE_TTL) {
|
|
366
|
-
loadIssues();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
loadAll();
|
|
371
|
-
|
|
372
|
-
root._loadAll = loadAll;
|
|
373
|
-
return root;
|
|
374
|
-
},
|
|
375
|
-
|
|
376
|
-
onActivate() {
|
|
377
|
-
const pane = document.querySelector('.right-panel-pane[data-tab="linear"] .linear-tab');
|
|
378
|
-
if (pane?._loadAll) pane._loadAll();
|
|
379
|
-
},
|
|
380
|
-
});
|