Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions lib/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,56 @@ function discoverAllAgents() {
return getAll();
}

// Read all named-agent definitions from a project-local .claude/agents/ directory.
//
// projectDir: absolute path to the project root (cwd).
// Returns: array of { name, slug, description } objects, sorted by name.
// Returns [] if the directory does not exist or is unreadable — callers treat
// an empty array as "no project-local agents" and degrade gracefully.
//
// This reads the filesystem directly (no SDK subprocess) — it is synchronous
// and cheap. The agents directory is small (typically 0–20 files).
function readProjectAgents(projectDir) {
if (!projectDir || typeof projectDir !== "string") return [];
var agentsDir = path.join(projectDir, ".claude", "agents");
var entries;
try {
entries = fs.readdirSync(agentsDir);
} catch (e) {
// Directory absent or unreadable — not an error.
return [];
}
var results = [];
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (!entry || !entry.endsWith(".md")) continue;
var slug = entry.slice(0, -3); // strip .md
if (!slug) continue;
var filePath = path.join(agentsDir, entry);
var raw;
try {
raw = fs.readFileSync(filePath, "utf8");
} catch (e) {
continue;
}
var parsed = parseFrontmatter(raw);
var name = (parsed && parsed.meta && parsed.meta.name) ? String(parsed.meta.name) : slug;
var description = (parsed && parsed.meta && parsed.meta.description) ? String(parsed.meta.description) : "";
results.push({ name: name, slug: slug, description: description });
}
results.sort(function (a, b) {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
return results;
}

module.exports = {
AGENTS_SOURCE_DIR: AGENTS_SOURCE_DIR,
PLUGINS_CACHE_DIR: PLUGINS_CACHE_DIR,
parseFrontmatter: parseFrontmatter,
slugifyAgentName: slugifyAgentName,
readAgentToolsFromFile: readAgentToolsFromFile,
readProjectAgents: readProjectAgents,
// New SDK-backed surface:
refresh: refresh,
getAll: getAll,
Expand Down
11 changes: 11 additions & 0 deletions lib/project-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ function attachSessions(ctx) {
return true;
}

// lr-c1a2 — get_agents returns agents installed in the project's
// .claude/agents/ directory (not the SDK global catalog). Used by the
// @ mention dropdown in the session input to offer project-local agents.
if (msg.type === "get_agents") {
var projectAgents = [];
try { projectAgents = agentsModule.readProjectAgents(cwd); }
catch (e) { console.error("[project-sessions] get_agents failed:", e && e.message ? e.message : e); }
sendTo(ws, { type: "project_agents_list", agents: projectAgents });
return true;
}

// toggle_agent_favorite flips an agent's membership in chattable.json.
// Required field: name.
if (msg.type === "toggle_agent_favorite") {
Expand Down
68 changes: 68 additions & 0 deletions lib/public/css/at-agents.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* ==========================================================================
@ Agent Autocomplete Dropdown (lr-c1a2)
========================================================================== */

.at-agents-menu {
display: none;
position: absolute;
bottom: 100%;
left: 0;
right: 0;
max-height: 220px;
overflow-y: auto;
background: var(--sidebar-bg);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 6px;
box-shadow: 0 4px 12px rgba(var(--shadow-rgb), 0.15);
z-index: 11;
}

.at-agents-menu.visible { display: block; }

.at-agents-item {
display: flex;
align-items: baseline;
padding: 8px 14px;
cursor: pointer;
gap: 8px;
border-bottom: 1px solid var(--border-subtle);
}

.at-agents-item:last-child { border-bottom: none; }

.at-agents-item:hover,
.at-agents-item.active {
background: var(--sidebar-active);
}

.at-agents-name {
font-size: 13px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
flex-shrink: 0;
}

.at-agents-badge {
font-size: 10px;
font-weight: 600;
color: var(--text-dimmer);
background: var(--border-subtle);
border-radius: 4px;
padding: 1px 5px;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
flex-shrink: 0;
}

.at-agents-desc {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
9 changes: 9 additions & 0 deletions lib/public/modules/app-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { updateDmBadge, renderSidebarPresence, setMentionActive, renderUserStrip
import { refreshMobileChatSheet } from './sidebar-mobile.js';
import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
import { handleAgentsList, handleAgentFavoriteToggled } from './agent-picker.js';
import { handleProjectAgentsList, requestProjectAgents } from './at-agents.js';
import { handleFindInSessionResults } from './session-search.js';
import { handleInputSync, autoResize, resetAutoResize, builtinCommands, setScheduleBtnDisabled } from './input.js';
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved } from './tools.js';
Expand Down Expand Up @@ -459,6 +460,10 @@ export function processMessage(msg) {
handleAgentFavoriteToggled(msg);
break;

case "project_agents_list":
handleProjectAgentsList(msg);
break;

case "session_presence":
updateSessionPresence(msg.presence || {});
break;
Expand Down Expand Up @@ -498,6 +503,10 @@ export function processMessage(msg) {
break;

case "session_switched":
// Prefetch project agents so the @ menu is ready without a round-trip
// the first time the user types @. Best-effort; at-agents.js handles
// an empty list gracefully.
requestProjectAgents();
hideHomeHub();
// Clear any stale replay indicator from a prior interrupted switch
if (replayLoadingEl) replayLoadingEl.classList.add("hidden");
Expand Down
Loading
Loading