claudeck 1.1.1 → 1.2.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.
- package/README.md +30 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +11 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +51 -0
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +43 -10
- package/public/js/core/api.js +83 -0
- package/public/js/core/dom.js +15 -0
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +16 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +3 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/style.css +4 -0
- package/server/agent-loop.js +13 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +17 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// ── Message Recall — InputHistory class + keydown handler ──
|
|
2
|
+
|
|
3
|
+
export class InputHistory {
|
|
4
|
+
constructor(storageKey, maxSize = 100) {
|
|
5
|
+
this.storageKey = storageKey;
|
|
6
|
+
this.maxSize = maxSize;
|
|
7
|
+
this.entries = [];
|
|
8
|
+
this.index = -1;
|
|
9
|
+
this.draft = "";
|
|
10
|
+
this._load();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Add a sent message to history. Skips empty and consecutive duplicates. */
|
|
14
|
+
add(text) {
|
|
15
|
+
if (!text || !text.trim()) return;
|
|
16
|
+
if (this.entries.length > 0 && this.entries[this.entries.length - 1] === text) return;
|
|
17
|
+
this.entries.push(text);
|
|
18
|
+
if (this.entries.length > this.maxSize) this.entries.shift();
|
|
19
|
+
this.index = -1;
|
|
20
|
+
this._save();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Navigate backward (older). Returns entry text, or null if empty. */
|
|
24
|
+
previous(currentInput) {
|
|
25
|
+
if (this.entries.length === 0) return null;
|
|
26
|
+
if (this.index === -1) this.draft = currentInput || "";
|
|
27
|
+
if (this.index < this.entries.length - 1) this.index++;
|
|
28
|
+
return this.entries[this.entries.length - 1 - this.index];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Navigate forward (newer). Returns entry text, or draft when past the end. */
|
|
32
|
+
next() {
|
|
33
|
+
if (this.index <= 0) {
|
|
34
|
+
this.index = -1;
|
|
35
|
+
return this.draft;
|
|
36
|
+
}
|
|
37
|
+
this.index--;
|
|
38
|
+
return this.entries[this.entries.length - 1 - this.index];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Cancel navigation and return to draft. */
|
|
42
|
+
cancel() {
|
|
43
|
+
const draft = this.draft;
|
|
44
|
+
this.index = -1;
|
|
45
|
+
this.draft = "";
|
|
46
|
+
return draft;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Reset navigation state (call after sending or typing). */
|
|
50
|
+
reset() {
|
|
51
|
+
this.index = -1;
|
|
52
|
+
this.draft = "";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether the user is currently navigating history. */
|
|
56
|
+
get isNavigating() {
|
|
57
|
+
return this.index !== -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Return all entries newest-first (for popover display). */
|
|
61
|
+
getAll() {
|
|
62
|
+
return [...this.entries].reverse();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @private Load from localStorage. */
|
|
66
|
+
_load() {
|
|
67
|
+
try {
|
|
68
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
69
|
+
if (raw) this.entries = JSON.parse(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
this.entries = [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @private Save to localStorage. */
|
|
76
|
+
_save() {
|
|
77
|
+
try {
|
|
78
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.entries));
|
|
79
|
+
} catch {
|
|
80
|
+
/* localStorage full — silently degrade */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle ArrowUp / ArrowDown / Escape for input history navigation.
|
|
87
|
+
* Returns true if the event was consumed (same contract as handleAutocompleteKeydown).
|
|
88
|
+
*/
|
|
89
|
+
export function handleHistoryKeydown(e, pane, history) {
|
|
90
|
+
const input = pane.messageInput;
|
|
91
|
+
|
|
92
|
+
if (e.key === "ArrowUp" && (input.value === "" || history.isNavigating)) {
|
|
93
|
+
const prev = history.previous(input.value);
|
|
94
|
+
if (prev !== null) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
input.value = prev;
|
|
97
|
+
triggerResize(input);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (e.key === "ArrowDown" && history.isNavigating) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
input.value = history.next();
|
|
105
|
+
triggerResize(input);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (e.key === "Escape" && history.isNavigating) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
input.value = history.cancel();
|
|
112
|
+
triggerResize(input);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function triggerResize(textarea) {
|
|
120
|
+
textarea.style.height = "auto";
|
|
121
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 200) + "px";
|
|
122
|
+
}
|
|
@@ -7,7 +7,7 @@ import { commandRegistry, registerCommand } from '../ui/commands.js';
|
|
|
7
7
|
import { panes } from '../ui/parallel.js';
|
|
8
8
|
import { loadSessions } from './sessions.js';
|
|
9
9
|
import { loadStats } from './cost-dashboard.js';
|
|
10
|
-
import { showWhalyPlaceholder } from '../ui/messages.js';
|
|
10
|
+
import { showWhalyPlaceholder, addSkillUsedMessage } from '../ui/messages.js';
|
|
11
11
|
|
|
12
12
|
export async function loadProjects() {
|
|
13
13
|
try {
|
|
@@ -91,11 +91,15 @@ export function updateHeaderProjectName() {
|
|
|
91
91
|
$.headerProjectName.textContent = opt && opt.value ? opt.textContent : "";
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Skill lookup map — exported so chat.js can look up model-invoked skills
|
|
95
|
+
export const skillLookup = new Map();
|
|
96
|
+
|
|
94
97
|
export async function loadProjectCommands() {
|
|
95
98
|
// Remove old project commands and skills
|
|
96
99
|
for (const [name, cmd] of Object.entries(commandRegistry)) {
|
|
97
100
|
if (cmd.category === "project" || cmd.category === "skill") delete commandRegistry[name];
|
|
98
101
|
}
|
|
102
|
+
skillLookup.clear();
|
|
99
103
|
|
|
100
104
|
const cwd = $.projectSelect.value;
|
|
101
105
|
if (!cwd) return;
|
|
@@ -109,12 +113,23 @@ export async function loadProjectCommands() {
|
|
|
109
113
|
if (!slug || commandRegistry[slug]) continue;
|
|
110
114
|
const hasArgs = c.prompt.includes("$ARGUMENTS");
|
|
111
115
|
const label = c.source === "skill" ? `${c.description}` : (c.description || c.command);
|
|
116
|
+
|
|
117
|
+
// Build skill lookup map
|
|
118
|
+
if (c.source === "skill") {
|
|
119
|
+
skillLookup.set(slug, { description: label, scope: "project" });
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
registerCommand(slug, {
|
|
113
123
|
category: c.source === "skill" ? "skill" : "project",
|
|
114
124
|
description: label,
|
|
115
125
|
needsArgs: hasArgs,
|
|
116
126
|
argumentHint: c.argumentHint || "",
|
|
117
127
|
execute(args, pane) {
|
|
128
|
+
// Show "Skill used" message for skills
|
|
129
|
+
if (c.source === "skill") {
|
|
130
|
+
addSkillUsedMessage(slug, c.description, pane);
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
let prompt = c.prompt;
|
|
119
134
|
if (hasArgs) {
|
|
120
135
|
prompt = prompt.replace(/\$ARGUMENTS/g, args || "");
|
|
@@ -46,37 +46,39 @@ export async function loadSessions(searchTerm) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function renderSessions(sessions) {
|
|
49
|
+
function renderSessions(sessions, append = false) {
|
|
50
50
|
const sessionId = getState("sessionId");
|
|
51
|
-
$.sessionList.innerHTML = "";
|
|
52
|
-
|
|
53
|
-
// Empty state: no project selected
|
|
54
|
-
const cwd = $.projectSelect.value;
|
|
55
|
-
if (!cwd) {
|
|
56
|
-
$.sessionList.innerHTML = `
|
|
57
|
-
<div class="session-empty">
|
|
58
|
-
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
59
|
-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
|
60
|
-
</svg>
|
|
61
|
-
<span>Select a project to view sessions</span>
|
|
62
|
-
</div>`;
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
51
|
+
if (!append) $.sessionList.innerHTML = "";
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
if (!append) {
|
|
54
|
+
// Empty state: no project selected
|
|
55
|
+
const cwd = $.projectSelect.value;
|
|
56
|
+
if (!cwd) {
|
|
57
|
+
$.sessionList.innerHTML = `
|
|
58
|
+
<div class="session-empty">
|
|
59
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
60
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
|
61
|
+
</svg>
|
|
62
|
+
<span>Select a project to view sessions</span>
|
|
63
|
+
</div>`;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Empty state: no sessions found
|
|
68
|
+
if (sessions.length === 0) {
|
|
69
|
+
const isSearch = $.sessionSearchInput.value.trim();
|
|
70
|
+
$.sessionList.innerHTML = `
|
|
71
|
+
<div class="session-empty">
|
|
72
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
73
|
+
${isSearch
|
|
74
|
+
? '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'
|
|
75
|
+
: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'}
|
|
76
|
+
</svg>
|
|
77
|
+
<span>${isSearch ? 'No matching sessions' : 'No sessions yet'}</span>
|
|
78
|
+
${!isSearch ? '<span class="session-empty-hint">Start a new conversation to create one</span>' : ''}
|
|
79
|
+
</div>`;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
for (const s of sessions) {
|
|
@@ -91,9 +93,12 @@ function renderSessions(sessions) {
|
|
|
91
93
|
const displayTitle = s.title || s.project_name || "Session";
|
|
92
94
|
const isPinned = s.pinned === 1;
|
|
93
95
|
const summaryTooltip = s.summary ? escapeHtml(s.summary) : "";
|
|
96
|
+
const forkIndicator = s.parent_session_id
|
|
97
|
+
? `<span class="session-fork-badge" title="Forked session"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg></span>`
|
|
98
|
+
: "";
|
|
94
99
|
li.innerHTML = `
|
|
95
100
|
<div class="session-card-header">
|
|
96
|
-
<span class="session-title" title="${escapeHtml(displayTitle)}">${escapeHtml(displayTitle)}</span>
|
|
101
|
+
<span class="session-title" title="${escapeHtml(displayTitle)}">${forkIndicator}${escapeHtml(displayTitle)}</span>
|
|
97
102
|
${modeBadge}
|
|
98
103
|
<span class="session-card-actions">
|
|
99
104
|
<button class="session-pin${isPinned ? " pinned" : ""}" title="${isPinned ? "Unpin" : "Pin"} session">
|
|
@@ -305,6 +310,48 @@ function showSessionContextMenu(e, session) {
|
|
|
305
310
|
});
|
|
306
311
|
sessionCtxMenu.appendChild(summaryBtn);
|
|
307
312
|
|
|
313
|
+
// View Parent (only for forked sessions)
|
|
314
|
+
if (session.parent_session_id) {
|
|
315
|
+
const parentBtn = document.createElement("button");
|
|
316
|
+
parentBtn.innerHTML = `<span class="ctx-label">View Parent Session</span><span class="ctx-value">Switch to parent</span>`;
|
|
317
|
+
parentBtn.addEventListener("click", async () => {
|
|
318
|
+
hideSessionContextMenu();
|
|
319
|
+
setState("sessionId", session.parent_session_id);
|
|
320
|
+
$.messagesDiv.innerHTML = "";
|
|
321
|
+
await loadMessages(session.parent_session_id);
|
|
322
|
+
loadSessions();
|
|
323
|
+
});
|
|
324
|
+
sessionCtxMenu.appendChild(parentBtn);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// View Forks
|
|
328
|
+
const forksBtn = document.createElement("button");
|
|
329
|
+
forksBtn.innerHTML = `<span class="ctx-label">View Forks</span><span class="ctx-value">Loading...</span>`;
|
|
330
|
+
forksBtn.addEventListener("click", async () => {
|
|
331
|
+
const branches = await api.fetchBranches(session.id);
|
|
332
|
+
if (branches.length === 0) {
|
|
333
|
+
forksBtn.querySelector(".ctx-value").textContent = "No forks";
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
hideSessionContextMenu();
|
|
337
|
+
// Show back header + filtered fork list
|
|
338
|
+
$.sessionList.innerHTML = "";
|
|
339
|
+
const backHeader = document.createElement("div");
|
|
340
|
+
backHeader.className = "session-forks-back";
|
|
341
|
+
backHeader.innerHTML = `<button class="session-forks-back-btn">← All Sessions</button><span class="session-forks-label">Forks of "${escapeHtml((session.title || session.project_name || "Session").slice(0, 30))}"</span>`;
|
|
342
|
+
backHeader.querySelector(".session-forks-back-btn").addEventListener("click", () => {
|
|
343
|
+
loadSessions($.sessionSearchInput.value.trim() || undefined);
|
|
344
|
+
});
|
|
345
|
+
$.sessionList.appendChild(backHeader);
|
|
346
|
+
renderSessions(branches, true);
|
|
347
|
+
});
|
|
348
|
+
sessionCtxMenu.appendChild(forksBtn);
|
|
349
|
+
// Eagerly load fork count
|
|
350
|
+
api.fetchBranches(session.id).then(branches => {
|
|
351
|
+
const val = forksBtn.querySelector(".ctx-value");
|
|
352
|
+
if (val) val.textContent = branches.length > 0 ? `${branches.length} fork${branches.length > 1 ? "s" : ""}` : "No forks";
|
|
353
|
+
});
|
|
354
|
+
|
|
308
355
|
sessionCtxMenu.style.left = e.clientX + "px";
|
|
309
356
|
sessionCtxMenu.style.top = e.clientY + "px";
|
|
310
357
|
document.body.appendChild(sessionCtxMenu);
|
package/public/js/main.js
CHANGED
|
@@ -22,6 +22,8 @@ import './features/prompts.js';
|
|
|
22
22
|
import './features/workflows.js';
|
|
23
23
|
import './features/agents.js';
|
|
24
24
|
import './ui/status-bar.js';
|
|
25
|
+
import './ui/notification-bell.js';
|
|
26
|
+
import './ui/notification-history.js';
|
|
25
27
|
import './features/cost-dashboard.js';
|
|
26
28
|
import './features/analytics.js';
|
|
27
29
|
import './features/telegram.js';
|
|
@@ -46,6 +48,7 @@ import './panels/tips-feed.js';
|
|
|
46
48
|
import './panels/assistant-bot.js';
|
|
47
49
|
import './panels/memory.js';
|
|
48
50
|
import './panels/dev-docs.js';
|
|
51
|
+
import './panels/skills-manager.js';
|
|
49
52
|
|
|
50
53
|
// Auto-discover and load tab-sdk plugins from /js/plugins/
|
|
51
54
|
import { loadPlugins } from './core/plugin-loader.js';
|