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
|
@@ -31,21 +31,37 @@ function getLangLabel(lang) {
|
|
|
31
31
|
export function renderMarkdown(text) {
|
|
32
32
|
let html = escapeHtml(text);
|
|
33
33
|
|
|
34
|
-
// ──
|
|
34
|
+
// ── Placeholder system ──
|
|
35
|
+
// Extract code blocks and inline code into placeholders FIRST to protect
|
|
36
|
+
// their content from text-level regex passes (bold, italic, links, etc.)
|
|
37
|
+
const placeholders = [];
|
|
38
|
+
function placeholder(content) {
|
|
39
|
+
placeholders.push(content);
|
|
40
|
+
return `\x00PH${placeholders.length - 1}\x00`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Code blocks — extract to placeholders ──
|
|
35
44
|
html = html.replace(
|
|
36
45
|
/```(\w*)\n([\s\S]*?)```/g,
|
|
37
46
|
(_, lang, code) => {
|
|
38
47
|
const langClass = lang ? `language-${lang}` : "";
|
|
39
48
|
const label = getLangLabel(lang);
|
|
40
49
|
const headerHtml = label
|
|
41
|
-
? `<div class="code-block-header"><span class="code-lang-label">${
|
|
50
|
+
? `<div class="code-block-header"><span class="code-lang-label">${label}</span></div>`
|
|
42
51
|
: "";
|
|
43
|
-
|
|
52
|
+
const wrappedCode = code
|
|
53
|
+
.replace(/\n$/, "")
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map(line => `<span class="code-line">${line}</span>`)
|
|
56
|
+
.join("\n");
|
|
57
|
+
return placeholder(`<div class="code-block-wrapper">${headerHtml}<pre><code class="${langClass}" data-lang="${lang}">${wrappedCode}</code></pre></div>`);
|
|
44
58
|
}
|
|
45
59
|
);
|
|
46
60
|
|
|
47
|
-
// ── Inline code ──
|
|
48
|
-
html = html.replace(/`([^`]+)`/g,
|
|
61
|
+
// ── Inline code — extract to placeholders ──
|
|
62
|
+
html = html.replace(/`([^`]+)`/g, (_, code) => {
|
|
63
|
+
return placeholder(`<code class="inline-code">${code}</code>`);
|
|
64
|
+
});
|
|
49
65
|
|
|
50
66
|
// ── Bold + Italic combined ──
|
|
51
67
|
html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>");
|
|
@@ -69,7 +85,6 @@ export function renderMarkdown(text) {
|
|
|
69
85
|
html = html.replace(/^---+$/gm, '<hr class="md-hr">');
|
|
70
86
|
|
|
71
87
|
// ── Blockquotes ──
|
|
72
|
-
// Match consecutive lines starting with >
|
|
73
88
|
html = html.replace(/(?:^> (.*)$\n?)+/gm, (match) => {
|
|
74
89
|
const lines = match.trim().split("\n").map(l => l.replace(/^> ?/, "")).join("<br>");
|
|
75
90
|
return `<blockquote class="md-blockquote">${lines}</blockquote>\n`;
|
|
@@ -78,8 +93,13 @@ export function renderMarkdown(text) {
|
|
|
78
93
|
// ── Links ──
|
|
79
94
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="md-link" target="_blank" rel="noopener">$1</a>');
|
|
80
95
|
|
|
96
|
+
// ── Auto-link bare URLs (not already inside an <a> tag or href attribute) ──
|
|
97
|
+
html = html.replace(
|
|
98
|
+
/(?<!href="|">)(https?:\/\/[^\s<>"'`)\]]+)/g,
|
|
99
|
+
'<a href="$1" class="md-link" target="_blank" rel="noopener">$1</a>'
|
|
100
|
+
);
|
|
101
|
+
|
|
81
102
|
// ── Tables ──
|
|
82
|
-
// Match table blocks: header row, separator row, then data rows
|
|
83
103
|
html = html.replace(
|
|
84
104
|
/(?:^\|(.+)\|$\n^\|[-| :]+\|$\n(?:^\|(.+)\|$\n?)*)/gm,
|
|
85
105
|
(match) => {
|
|
@@ -89,7 +109,6 @@ export function renderMarkdown(text) {
|
|
|
89
109
|
const parseRow = (row) =>
|
|
90
110
|
row.split("|").filter((_, i, arr) => i > 0 && i < arr.length - 1).map(c => c.trim());
|
|
91
111
|
|
|
92
|
-
// Parse alignment from separator row
|
|
93
112
|
const sepCells = parseRow(rows[1]);
|
|
94
113
|
const aligns = sepCells.map(c => {
|
|
95
114
|
if (c.startsWith(":") && c.endsWith(":")) return "center";
|
|
@@ -119,22 +138,43 @@ export function renderMarkdown(text) {
|
|
|
119
138
|
);
|
|
120
139
|
|
|
121
140
|
// ── Ordered lists ──
|
|
122
|
-
// Match consecutive lines starting with digits followed by . or )
|
|
123
141
|
html = html.replace(/(?:^\d+[.)]\s+.+$\n?)+/gm, (match) => {
|
|
124
142
|
const items = match.trim().split("\n").map(l => l.replace(/^\d+[.)]\s+/, ""));
|
|
125
143
|
return '<ol class="md-list md-ol">' + items.map(i => `<li>${i}</li>`).join("") + "</ol>\n";
|
|
126
144
|
});
|
|
127
145
|
|
|
146
|
+
// ── Task lists (before general unordered lists) ──
|
|
147
|
+
html = html.replace(/(?:^[-*+]\s+\[[ xX]\]\s+.+$\n?)+/gm, (match) => {
|
|
148
|
+
const items = match.trim().split("\n").map(l => {
|
|
149
|
+
const checked = /^[-*+]\s+\[x\]/i.test(l);
|
|
150
|
+
const text = l.replace(/^[-*+]\s+\[[ xX]\]\s+/, "");
|
|
151
|
+
const checkbox = `<input type="checkbox" class="md-checkbox" ${checked ? "checked" : ""} disabled>`;
|
|
152
|
+
const spanClass = checked ? ' class="task-text-done"' : "";
|
|
153
|
+
return `<li>${checkbox}<span${spanClass}>${text}</span></li>`;
|
|
154
|
+
});
|
|
155
|
+
return '<ul class="md-list md-task-list">' + items.join("") + "</ul>\n";
|
|
156
|
+
});
|
|
157
|
+
|
|
128
158
|
// ── Unordered lists ──
|
|
129
|
-
// Match consecutive lines starting with -, *, or +
|
|
130
159
|
html = html.replace(/(?:^[-*+]\s+.+$\n?)+/gm, (match) => {
|
|
131
160
|
const items = match.trim().split("\n").map(l => l.replace(/^[-*+]\s+/, ""));
|
|
132
161
|
return '<ul class="md-list md-ul">' + items.map(i => `<li>${i}</li>`).join("") + "</ul>\n";
|
|
133
162
|
});
|
|
134
163
|
|
|
135
164
|
// ── Line breaks ──
|
|
165
|
+
html = html.replace(/\n{3,}/g, "\n\n");
|
|
136
166
|
html = html.replace(/\n/g, "<br>");
|
|
137
167
|
|
|
168
|
+
// Remove redundant <br> around block elements that already have CSS margins
|
|
169
|
+
html = html.replace(/(<br>)+(<(?:h[1-4]|ul|ol|div|table|blockquote|hr)[\s>])/g, "$2");
|
|
170
|
+
html = html.replace(/(<\/(?:h[1-4]|ul|ol|div|table|blockquote|hr)>)(<br>)+/g, "$1");
|
|
171
|
+
// Also clean <br> around placeholder tokens (code blocks are block-level)
|
|
172
|
+
html = html.replace(/(<br>)+(\x00PH\d+\x00)/g, "$2");
|
|
173
|
+
html = html.replace(/(\x00PH\d+\x00)(<br>)+/g, "$1");
|
|
174
|
+
|
|
175
|
+
// ── Restore placeholders ──
|
|
176
|
+
html = html.replace(/\x00PH(\d+)\x00/g, (_, i) => placeholders[parseInt(i)]);
|
|
177
|
+
|
|
138
178
|
return html;
|
|
139
179
|
}
|
|
140
180
|
|
|
@@ -143,9 +183,23 @@ export function highlightCodeBlocks(container) {
|
|
|
143
183
|
container.querySelectorAll("pre code").forEach((block) => {
|
|
144
184
|
if (block.dataset.highlighted === "yes") return;
|
|
145
185
|
try {
|
|
146
|
-
// Highlight both language-tagged and untagged blocks (auto-detect)
|
|
147
186
|
hljs.highlightElement(block);
|
|
148
187
|
} catch { /* ignore unsupported languages */ }
|
|
188
|
+
// Re-wrap lines for CSS line numbering after highlight.js processes the block
|
|
189
|
+
wrapCodeLinesInBlock(block);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function wrapCodeLinesInBlock(block) {
|
|
194
|
+
if (block.querySelector(".code-line")) return;
|
|
195
|
+
const html = block.innerHTML;
|
|
196
|
+
const lines = html.split("\n");
|
|
197
|
+
block.innerHTML = lines.map(l => `<span class="code-line">${l}</span>`).join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function wrapCodeLines(container) {
|
|
201
|
+
container.querySelectorAll("pre code").forEach((block) => {
|
|
202
|
+
wrapCodeLinesInBlock(block);
|
|
149
203
|
});
|
|
150
204
|
}
|
|
151
205
|
|
package/public/js/ui/messages.js
CHANGED
|
@@ -6,9 +6,89 @@ import { getState, setState } from '../core/store.js';
|
|
|
6
6
|
import { $ } from '../core/dom.js';
|
|
7
7
|
import { getPane } from './parallel.js';
|
|
8
8
|
|
|
9
|
+
const WELCOME_GREETINGS = [
|
|
10
|
+
"What can I help you build today?",
|
|
11
|
+
"Ready when you are!",
|
|
12
|
+
"Let's create something great together",
|
|
13
|
+
"What's on your mind?",
|
|
14
|
+
"Let's turn your ideas into code",
|
|
15
|
+
"What are we working on today?",
|
|
16
|
+
"Got a bug to squash or a feature to ship?",
|
|
17
|
+
"Your next big idea starts here",
|
|
18
|
+
"Let's make something awesome",
|
|
19
|
+
"How can I help you today?",
|
|
20
|
+
"What challenge are we tackling today?",
|
|
21
|
+
"Let's get things done together",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function getRandomGreeting() {
|
|
25
|
+
return WELCOME_GREETINGS[Math.floor(Math.random() * WELCOME_GREETINGS.length)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getChatAreaMain() {
|
|
29
|
+
return document.querySelector('.chat-area-main');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function showWelcomeState() {
|
|
33
|
+
const chatMain = getChatAreaMain();
|
|
34
|
+
if (!chatMain) return;
|
|
35
|
+
// Create the welcome center element if it doesn't exist
|
|
36
|
+
let welcomeEl = chatMain.querySelector('.welcome-center');
|
|
37
|
+
if (!welcomeEl) {
|
|
38
|
+
welcomeEl = document.createElement('div');
|
|
39
|
+
welcomeEl.className = 'welcome-center';
|
|
40
|
+
welcomeEl.innerHTML = `<img class="whaly-welcome-img" src="/icons/whaly.png" alt="Whaly" draggable="false"><div class="welcome-greeting">${getRandomGreeting()}</div>`;
|
|
41
|
+
// Insert before the input-bar
|
|
42
|
+
const inputBar = chatMain.querySelector('.input-bar');
|
|
43
|
+
if (inputBar) {
|
|
44
|
+
chatMain.insertBefore(welcomeEl, inputBar);
|
|
45
|
+
} else {
|
|
46
|
+
chatMain.appendChild(welcomeEl);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// Update greeting text
|
|
50
|
+
const greetingEl = welcomeEl.querySelector('.welcome-greeting');
|
|
51
|
+
if (greetingEl) greetingEl.textContent = getRandomGreeting();
|
|
52
|
+
}
|
|
53
|
+
chatMain.classList.remove('welcome-exit');
|
|
54
|
+
chatMain.classList.add('welcome-state');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function exitWelcomeState() {
|
|
58
|
+
const chatMain = getChatAreaMain();
|
|
59
|
+
if (!chatMain || !chatMain.classList.contains('welcome-state')) return Promise.resolve();
|
|
60
|
+
|
|
61
|
+
return new Promise(resolve => {
|
|
62
|
+
chatMain.classList.add('welcome-exit');
|
|
63
|
+
|
|
64
|
+
const onEnd = () => {
|
|
65
|
+
chatMain.classList.remove('welcome-state', 'welcome-exit');
|
|
66
|
+
resolve();
|
|
67
|
+
};
|
|
68
|
+
// Listen for the fade-out animation on the welcome center
|
|
69
|
+
const welcomeEl = chatMain.querySelector('.welcome-center');
|
|
70
|
+
if (welcomeEl) {
|
|
71
|
+
welcomeEl.addEventListener('animationend', onEnd, { once: true });
|
|
72
|
+
} else {
|
|
73
|
+
onEnd();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isWelcomeStateActive() {
|
|
79
|
+
const chatMain = getChatAreaMain();
|
|
80
|
+
return chatMain?.classList.contains('welcome-state') || false;
|
|
81
|
+
}
|
|
82
|
+
|
|
9
83
|
export function showWhalyPlaceholder(pane) {
|
|
10
84
|
pane = pane || getPane(null);
|
|
11
85
|
removeWhalyPlaceholder(pane);
|
|
86
|
+
// Use welcome state for the main (non-parallel) pane when the DOM supports it
|
|
87
|
+
const parallelMode = getState("parallelMode");
|
|
88
|
+
if (!parallelMode && getChatAreaMain()) {
|
|
89
|
+
showWelcomeState();
|
|
90
|
+
}
|
|
91
|
+
// Always add the placeholder into the messages div (tests + parallel mode rely on this)
|
|
12
92
|
const el = document.createElement("div");
|
|
13
93
|
el.className = "whaly-placeholder";
|
|
14
94
|
el.innerHTML = `<img src="/icons/whaly.png" alt="Whaly" draggable="false"><div class="whaly-text">~ start chatting with claude ~</div><div class="whaly-hint">Type a message or select a prompt template</div>`;
|
|
@@ -24,6 +104,9 @@ export function removeWhalyPlaceholder(pane) {
|
|
|
24
104
|
export function addUserMessage(text, pane, images = [], filePaths = []) {
|
|
25
105
|
pane = pane || getPane(null);
|
|
26
106
|
removeWhalyPlaceholder(pane);
|
|
107
|
+
// Exit welcome state immediately (no animation — message renders right away)
|
|
108
|
+
const chatMain = getChatAreaMain();
|
|
109
|
+
if (chatMain) chatMain.classList.remove('welcome-state', 'welcome-exit');
|
|
27
110
|
pane.currentAssistantMsg = null;
|
|
28
111
|
const div = document.createElement("div");
|
|
29
112
|
div.className = "msg msg-user";
|
|
@@ -58,7 +141,8 @@ export function addUserMessage(text, pane, images = [], filePaths = []) {
|
|
|
58
141
|
}
|
|
59
142
|
|
|
60
143
|
pane.messagesDiv.appendChild(div);
|
|
61
|
-
|
|
144
|
+
// User pressed send — always pull them to the bottom regardless of scroll position.
|
|
145
|
+
scrollToBottom(pane, { force: true });
|
|
62
146
|
}
|
|
63
147
|
|
|
64
148
|
function renderChatImages(images, container) {
|
|
@@ -343,6 +427,9 @@ export function renderMessagesIntoPane(messages, pane) {
|
|
|
343
427
|
showWhalyPlaceholder(pane);
|
|
344
428
|
return;
|
|
345
429
|
}
|
|
430
|
+
// Exit welcome state when loading existing messages
|
|
431
|
+
const chatMain = getChatAreaMain();
|
|
432
|
+
if (chatMain) chatMain.classList.remove('welcome-state', 'welcome-exit');
|
|
346
433
|
// Track last assistant message ID for fork button placement
|
|
347
434
|
let lastAssistantMsgEl = null;
|
|
348
435
|
let lastAssistantMsgId = null;
|
|
@@ -432,6 +519,15 @@ export function renderMessagesIntoPane(messages, pane) {
|
|
|
432
519
|
highlightCodeBlocks(pane.messagesDiv);
|
|
433
520
|
addCopyButtons(pane.messagesDiv);
|
|
434
521
|
renderMermaidBlocks(pane.messagesDiv);
|
|
522
|
+
// Loading a saved session: jump to the latest message and re-engage follow mode.
|
|
523
|
+
// Per-message scrollToBottom calls during the render loop are no-ops because
|
|
524
|
+
// scrollTop=0 + a tall scrollHeight fails the near-bottom check; the final
|
|
525
|
+
// forced scroll lands the user where they expect (newest message visible).
|
|
526
|
+
// Skip when rendering into a detached temp container (e.g. prependOlderMessages),
|
|
527
|
+
// which only uses this function for DOM rendering and manages scroll itself.
|
|
528
|
+
if (pane.messagesDiv && pane.messagesDiv.isConnected) {
|
|
529
|
+
scrollToBottom(pane, { force: true });
|
|
530
|
+
}
|
|
435
531
|
}
|
|
436
532
|
|
|
437
533
|
function addForkButton(msgEl, messageId) {
|
package/public/js/ui/parallel.js
CHANGED
|
@@ -4,6 +4,26 @@ import { getState, setState } from '../core/store.js';
|
|
|
4
4
|
import { CHAT_IDS } from '../core/constants.js';
|
|
5
5
|
import { handleAutocompleteKeydown, handleSlashAutocomplete } from './commands.js';
|
|
6
6
|
import { handleHistoryKeydown } from '../features/input-history.js';
|
|
7
|
+
import { isNearBottom } from '../core/utils.js';
|
|
8
|
+
|
|
9
|
+
// Wire stick-to-bottom tracking onto a pane. Updates pane.followBottom as the
|
|
10
|
+
// user scrolls so scrollToBottom() knows whether to yank them or leave them be.
|
|
11
|
+
function attachScrollTracking(pane) {
|
|
12
|
+
pane.followBottom = true;
|
|
13
|
+
pane.hasNewBelow = false;
|
|
14
|
+
const el = pane.messagesDiv;
|
|
15
|
+
if (!el || !el.addEventListener) return;
|
|
16
|
+
el.addEventListener("scroll", () => {
|
|
17
|
+
const atBottom = isNearBottom(el);
|
|
18
|
+
pane.followBottom = atBottom;
|
|
19
|
+
if (atBottom && pane.hasNewBelow) {
|
|
20
|
+
pane.hasNewBelow = false;
|
|
21
|
+
window.dispatchEvent(new CustomEvent("claudeck:scroll-state", {
|
|
22
|
+
detail: { chatId: pane.chatId, hasNewBelow: false },
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
}, { passive: true });
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
// Panes map — chatId -> pane state object
|
|
9
29
|
export const panes = new Map();
|
|
@@ -15,7 +35,7 @@ export function getPane(chatId) {
|
|
|
15
35
|
|
|
16
36
|
export function initSinglePane() {
|
|
17
37
|
panes.clear();
|
|
18
|
-
|
|
38
|
+
const pane = {
|
|
19
39
|
chatId: null,
|
|
20
40
|
messagesDiv: $.messagesDiv,
|
|
21
41
|
messageInput: $.messageInput,
|
|
@@ -25,7 +45,12 @@ export function initSinglePane() {
|
|
|
25
45
|
currentAssistantMsg: null,
|
|
26
46
|
autocompleteEl: document.getElementById("slash-autocomplete"),
|
|
27
47
|
_autocompleteIndex: -1,
|
|
28
|
-
|
|
48
|
+
_messageQueue: [],
|
|
49
|
+
_queuePaused: false,
|
|
50
|
+
_queuePauseReason: null,
|
|
51
|
+
};
|
|
52
|
+
attachScrollTracking(pane);
|
|
53
|
+
panes.set(null, pane);
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
// Initialize on load
|
|
@@ -88,8 +113,13 @@ export function createChatPane(chatId, index) {
|
|
|
88
113
|
statusEl: header.querySelector(".chat-pane-status"),
|
|
89
114
|
autocompleteEl: paneAutocomplete,
|
|
90
115
|
_autocompleteIndex: -1,
|
|
116
|
+
_messageQueue: [],
|
|
117
|
+
_queuePaused: false,
|
|
118
|
+
_queuePauseReason: null,
|
|
91
119
|
};
|
|
92
120
|
|
|
121
|
+
attachScrollTracking(state);
|
|
122
|
+
|
|
93
123
|
paneSendBtn.addEventListener("click", () => sendMessage(state));
|
|
94
124
|
paneStopBtn.addEventListener("click", () => stopGeneration(state));
|
|
95
125
|
|
|
@@ -6,7 +6,6 @@ import { initTabSDK } from "./tab-sdk.js";
|
|
|
6
6
|
const STORAGE_KEY = "claudeck-right-panel";
|
|
7
7
|
const TAB_KEY = "claudeck-right-panel-tab";
|
|
8
8
|
const WIDTH_KEY = "claudeck-right-panel-width";
|
|
9
|
-
const OLD_LINEAR_KEY = "claudeck-linear-panel";
|
|
10
9
|
const MIN_WIDTH = 200;
|
|
11
10
|
const MAX_WIDTH_RATIO = 0.6; // 60vw
|
|
12
11
|
|
|
@@ -82,13 +81,6 @@ function applyTab(tabName) {
|
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
function initRightPanel() {
|
|
85
|
-
// Migrate old linear panel localStorage key
|
|
86
|
-
const oldState = localStorage.getItem(OLD_LINEAR_KEY);
|
|
87
|
-
if (oldState && !localStorage.getItem(STORAGE_KEY)) {
|
|
88
|
-
localStorage.setItem(STORAGE_KEY, oldState);
|
|
89
|
-
localStorage.removeItem(OLD_LINEAR_KEY);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
84
|
// Tab click handlers
|
|
93
85
|
$.rightPanel.querySelectorAll(".right-panel-tab").forEach((btn) => {
|
|
94
86
|
btn.addEventListener("click", () => {
|
package/public/style.css
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
@import url("css/panels/skills-manager.css");
|
|
43
43
|
@import url("css/features/telegram.css");
|
|
44
44
|
@import url("css/features/voice-input.css");
|
|
45
|
+
@import url("css/features/message-queue.css");
|
|
45
46
|
@import url("css/features/retro-terminal.css");
|
|
46
47
|
@import url("css/features/welcome.css");
|
|
47
48
|
@import url("css/features/tour.css");
|
|
Binary file
|