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.
Files changed (37) hide show
  1. package/README.md +30 -4
  2. package/config/skillsmp-config.json +5 -0
  3. package/db.js +248 -0
  4. package/package.json +11 -2
  5. package/public/css/panels/git-panel.css +220 -0
  6. package/public/css/panels/skills-manager.css +975 -0
  7. package/public/css/ui/input-history.css +109 -0
  8. package/public/css/ui/messages.css +51 -0
  9. package/public/css/ui/notification-bell.css +421 -0
  10. package/public/css/ui/sessions.css +41 -0
  11. package/public/css/ui/worktree.css +442 -0
  12. package/public/index.html +43 -10
  13. package/public/js/core/api.js +83 -0
  14. package/public/js/core/dom.js +15 -0
  15. package/public/js/features/background-sessions.js +11 -0
  16. package/public/js/features/chat.js +501 -3
  17. package/public/js/features/input-history.js +122 -0
  18. package/public/js/features/projects.js +16 -1
  19. package/public/js/features/sessions.js +77 -30
  20. package/public/js/main.js +3 -0
  21. package/public/js/panels/git-panel.js +385 -6
  22. package/public/js/panels/skills-manager.js +1005 -0
  23. package/public/js/ui/messages.js +58 -0
  24. package/public/js/ui/notification-bell.js +240 -0
  25. package/public/js/ui/notification-history.js +210 -0
  26. package/public/js/ui/parallel.js +11 -0
  27. package/public/js/ui/tab-sdk.js +1 -1
  28. package/public/style.css +4 -0
  29. package/server/agent-loop.js +13 -0
  30. package/server/notification-logger.js +27 -0
  31. package/server/routes/notifications.js +57 -1
  32. package/server/routes/sessions.js +41 -0
  33. package/server/routes/skills.js +454 -0
  34. package/server/routes/worktrees.js +93 -0
  35. package/server/utils/git-worktree.js +297 -0
  36. package/server/ws-handler.js +708 -629
  37. 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
- // Empty state: no sessions found
67
- if (sessions.length === 0) {
68
- const isSearch = $.sessionSearchInput.value.trim();
69
- $.sessionList.innerHTML = `
70
- <div class="session-empty">
71
- <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
72
- ${isSearch
73
- ? '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'
74
- : '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'}
75
- </svg>
76
- <span>${isSearch ? 'No matching sessions' : 'No sessions yet'}</span>
77
- ${!isSearch ? '<span class="session-empty-hint">Start a new conversation to create one</span>' : ''}
78
- </div>`;
79
- return;
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">&larr; 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';