Merge branch 'worktree-agent-afbb9437'
This commit is contained in:
commit
2ed952f19c
90 files changed
+270701
-32
No files matched your search
@@ -1182,6 +1182,13 @@ export class AppDatabase {
|
||||
return row ? { ...row, isPrimary: toBoolean(row.isPrimary) } : null;
|
||||
}
|
||||
|
||||
listWorkspaceLocalPaths(companyId: string): string[] {
|
||||
const rows = this.db
|
||||
.prepare("select local_path as localPath from workspaces where company_id = ? and local_path is not null")
|
||||
.all(companyId) as Array<{ localPath: string }>;
|
||||
return rows.map((r) => r.localPath).filter(Boolean);
|
||||
}
|
||||
|
||||
getAnyCompanyWorkspace(companyId: string): WorkspaceRecord | null {
|
||||
const row = this.db
|
||||
.prepare(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
dialog,
|
||||
Menu,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
net,
|
||||
protocol,
|
||||
session,
|
||||
@@ -206,7 +207,7 @@ export function createDesktopShellService({
|
||||
y: savedWindowState.bounds.y,
|
||||
minWidth: 1180,
|
||||
minHeight: 760,
|
||||
backgroundColor: "#13110f",
|
||||
backgroundColor: "#ffffff",
|
||||
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
|
||||
webPreferences: {
|
||||
preload: preloadPath,
|
||||
@@ -220,6 +221,9 @@ export function createDesktopShellService({
|
||||
},
|
||||
});
|
||||
|
||||
// Sync native theme with app to ensure Windows title bar matches content
|
||||
nativeTheme.themeSource = "light";
|
||||
|
||||
if (isDev && devServerUrl) {
|
||||
await mainWindow.loadURL(devServerUrl);
|
||||
} else {
|
||||
|
||||
@@ -50,13 +50,19 @@ export function registerSkillHandlers({
|
||||
return result;
|
||||
});
|
||||
|
||||
// ---- List all skills for a company ----
|
||||
// ---- List all skills for a company (also scans workspace .agents/skills/) ----
|
||||
registerHandle(IPC_CHANNELS.listSkills, async (_event, payload) => {
|
||||
const parsed = listSkillsSchema.parse(payload);
|
||||
const companyError = ensureCompanyExists(parsed.companyId);
|
||||
if (companyError) return companyError;
|
||||
const serviceError = requireSkillService();
|
||||
if (serviceError) return serviceError;
|
||||
// Scan workspace .agents/skills/ directories for new skills before listing
|
||||
try {
|
||||
await skillService!.scanWorkspaceSkills(parsed.companyId);
|
||||
} catch {
|
||||
// Non-fatal: scan failure should not block listing existing skills
|
||||
}
|
||||
return ok(skillService!.listForCompany(parsed.companyId));
|
||||
});
|
||||
|
||||
|
||||
@@ -701,6 +701,66 @@ export function createSkillService({ db, profileDir, logger }: SkillServiceDepen
|
||||
return inventory;
|
||||
}
|
||||
|
||||
// ---- Workspace .agents/skills/ scanning (Bug 6) ----
|
||||
|
||||
/**
|
||||
* Scan all workspace local paths for a company, looking for
|
||||
* `.agents/skills/` directories containing SKILL.md files.
|
||||
* Auto-imports any found skills that are not already in the database.
|
||||
* Cross-platform: uses path.join for correct separators.
|
||||
*/
|
||||
async function scanWorkspaceSkills(
|
||||
companyId: string,
|
||||
): Promise<SkillImportResult[]> {
|
||||
const allResults: SkillImportResult[] = [];
|
||||
|
||||
// 1. Scan user home directories: ~/.agents/skills/ and ~/.claude/skills/
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
||||
if (homeDir) {
|
||||
const homeScanDirs = [
|
||||
join(homeDir, ".agents", "skills"),
|
||||
join(homeDir, ".claude", "skills"),
|
||||
];
|
||||
for (const scanDir of homeScanDirs) {
|
||||
if (!existsSync(scanDir)) continue;
|
||||
try {
|
||||
const stat = statSync(scanDir);
|
||||
if (!stat.isDirectory()) continue;
|
||||
const results = await importFromLocal(companyId, scanDir);
|
||||
allResults.push(...results);
|
||||
logger.info(`Scanned home skills: ${scanDir} — imported ${results.length} skill(s)`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logger.warn(`Failed to scan home skills at ${scanDir}: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Scan workspace .agents/skills/ directories
|
||||
const workspacePaths = db.listWorkspaceLocalPaths(companyId);
|
||||
for (const wsPath of workspacePaths) {
|
||||
const agentsSkillsDir = join(wsPath, ".agents", "skills");
|
||||
if (!existsSync(agentsSkillsDir)) continue;
|
||||
|
||||
try {
|
||||
const stat = statSync(agentsSkillsDir);
|
||||
if (!stat.isDirectory()) continue;
|
||||
|
||||
const results = await importFromLocal(companyId, agentsSkillsDir);
|
||||
allResults.push(...results);
|
||||
logger.info(
|
||||
`Scanned workspace skills: ${agentsSkillsDir} — imported ${results.length} skill(s)`,
|
||||
);
|
||||
} catch (err) {
|
||||
// Log but don't fail the entire scan if one workspace has issues
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
logger.warn(`Failed to scan workspace skills at ${agentsSkillsDir}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
// ---- Public API ----
|
||||
|
||||
return {
|
||||
@@ -715,5 +775,6 @@ export function createSkillService({ db, profileDir, logger }: SkillServiceDepen
|
||||
removeFromAgent,
|
||||
checkUpdateStatus,
|
||||
listRuntimeSkillEntries,
|
||||
scanWorkspaceSkills,
|
||||
};
|
||||
}
|
||||
@@ -1213,7 +1213,7 @@ export function App() {
|
||||
documents={companyDocuments}
|
||||
companyId={currentCompany!.id}
|
||||
onDecide={async (state, note) => {
|
||||
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control bg-card border border-border shadow-sm.` }));
|
||||
unwrap(await window.agentCompany.decideApproval({ companyId: currentCompany!.id, approvalId: selectedApproval.id, state, decisionNote: note ?? `${state} from desktop control surface.` }));
|
||||
await refresh();
|
||||
}}
|
||||
onDeleted={async () => { setSelectedApprovalId(null); await refresh(); }}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function OnboardingHero({
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-0 items-center justify-center overflow-y-auto px-8 py-12 md:px-16">
|
||||
<div className="relative flex min-h-0 items-center justify-center overflow-hidden px-8 py-12 md:px-16">
|
||||
{/* Ambient gradient - subtle animated accent wash */}
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.035]"
|
||||
|
||||
@@ -187,7 +187,7 @@ export function CompanyOnboarding({
|
||||
}, [launchSummary, onDone, t]);
|
||||
|
||||
return (
|
||||
<div className="relative grid min-h-[100dvh] overflow-hidden bg-background md:grid-cols-[minmax(0,1fr)_540px]">
|
||||
<div className="relative grid h-[100dvh] overflow-hidden bg-background md:grid-cols-[minmax(0,1fr)_540px]">
|
||||
{/* Drag region for macOS frameless title bar */}
|
||||
<div className="sidebar-drag absolute inset-x-0 top-0 z-10 h-[38px]" />
|
||||
<OnboardingHero
|
||||
@@ -201,7 +201,7 @@ export function CompanyOnboarding({
|
||||
|
||||
<div className="min-h-0 overflow-y-auto border-l border-border bg-background">
|
||||
<div className="relative flex min-h-full items-center px-6 py-10 md:px-10">
|
||||
{canCancel && onCancel ? (
|
||||
{canCancel && onCancel && !isFirstLaunch ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
|
||||
@@ -2,8 +2,11 @@ import type { AutomationAction, AutomationStatus, AutomationTrigger, WorkflowSta
|
||||
import { ArrowsClockwise, Eye, Funnel, Lightning, Pause, Play, Plus, Robot, Timer, Trash } from "@phosphor-icons/react"
|
||||
import { DEPARTMENT_OPTIONS } from "@shared/constants"
|
||||
|
||||
export const ACCENT = "var(--primary)"
|
||||
export const LINE = "var(--border)"
|
||||
// ACCENT/LINE used in template literals for opacity suffixes like `${ACCENT}15`
|
||||
// Must be hex values (not CSS vars) so hex alpha notation works.
|
||||
// These are the primary/border colors from the OKLCH design tokens converted to hex.
|
||||
export const ACCENT = "#343434"
|
||||
export const LINE = "#ebebeb"
|
||||
|
||||
export const triggerConfig: Record<AutomationTrigger, { label: string; description: string; icon: typeof Lightning }> = {
|
||||
task_status_changed: { label: "Task Status Changed", description: "When a task transitions to a new status", icon: ArrowsClockwise },
|
||||
|
||||
@@ -145,11 +145,11 @@ export function ChannelSidebar({
|
||||
type="button"
|
||||
onClick={() => onSelectChannel(discussion.key)}
|
||||
className={`group flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left transition duration-150 ${
|
||||
selectedChannelKey === discussion.key ? "bg-muted" : "hover:bg-muted/50"
|
||||
selectedChannelKey === discussion.key ? "bg-accent" : "hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: "var(--primary)" }}>
|
||||
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} style={{ color: "var(--primary)" }} />
|
||||
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
<Hash size={14} weight={selectedChannelKey === discussion.key ? "fill" : "regular"} className="text-muted-foreground" />
|
||||
</div>
|
||||
<span className={`min-w-0 flex-1 truncate text-[13px] ${selectedChannelKey === discussion.key ? "font-semibold text-foreground" : "font-medium text-muted-foreground"}`}>
|
||||
{discussion.label}
|
||||
|
||||
@@ -13,10 +13,10 @@ import { getAgentInitials, formatMessageTime } from "../../lib/formatters"
|
||||
import { DEPARTMENT_COLORS } from "@shared/constants"
|
||||
|
||||
|
||||
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
|
||||
urgent: { label: "Urgent", color: "var(--destructive)", bg: "var(--destructive)" },
|
||||
normal: { label: "Normal", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
|
||||
low: { label: "Low", color: "var(--muted-foreground)", bg: "var(--muted-foreground)" },
|
||||
const priorityConfig: Record<string, { label: string; className: string }> = {
|
||||
urgent: { label: "Urgent", className: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" },
|
||||
normal: { label: "Normal", className: "bg-muted text-muted-foreground" },
|
||||
low: { label: "Low", className: "bg-muted text-muted-foreground" },
|
||||
}
|
||||
|
||||
function getDepartmentColor(department: DepartmentId | null | undefined): string {
|
||||
@@ -87,8 +87,7 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
) : null}
|
||||
{agent?.department ? (
|
||||
<span
|
||||
className="shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em]"
|
||||
style={{ backgroundColor: `${deptColor}15`, color: deptColor }}
|
||||
className="shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.08em] text-muted-foreground"
|
||||
>
|
||||
{agent.department.replace("_", " ")}
|
||||
</span>
|
||||
@@ -98,14 +97,10 @@ export const MessageBubble = memo(function MessageBubble({
|
||||
</span>
|
||||
{message.priority !== "normal" ? (
|
||||
<span
|
||||
className="flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.06em]"
|
||||
style={{
|
||||
backgroundColor: priorityConfig[message.priority].bg,
|
||||
color: priorityConfig[message.priority].color,
|
||||
}}
|
||||
className={`flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.06em] ${priorityConfig[message.priority]?.className ?? ""}`}
|
||||
>
|
||||
{message.priority === "urgent" ? <WarningCircle size={10} weight="fill" /> : null}
|
||||
{priorityConfig[message.priority].label}
|
||||
{priorityConfig[message.priority]?.label}
|
||||
</span>
|
||||
) : null}
|
||||
{isUnread ? (
|
||||
|
||||
@@ -99,7 +99,7 @@ export function DocumentCenter({ companyId, documents, agents, projects, goals,
|
||||
<Modal open={editingId !== null} title={activeDocument ? activeDocument.title : "New document"} onClose={() => setEditingId(null)} width="760px">
|
||||
<DocumentEditor companyId={companyId} agents={agents} projects={projects} goals={goals} document={editingId === "__new__" ? null : activeDocument} onSaved={async () => { setEditingId(null); await onRefresh(); }} />
|
||||
</Modal>
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete document" message="This removes the document from the company knowledge bg-card border border-border shadow-sm." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
<ConfirmDialog open={deleteId !== null} title="Delete document" message="This removes the document from the company knowledge base." onCancel={() => setDeleteId(null)} onConfirm={async () => { if (!deleteId) return; const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId }); setDeleteId(null); if (result.ok) await onRefresh(); }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -80,11 +80,11 @@ export function Panel({
|
||||
children: ReactNode;
|
||||
action?: ReactNode;
|
||||
className?: string;
|
||||
tone?: "plain" | "bg-card border border-border shadow-sm";
|
||||
tone?: "plain" | "surface";
|
||||
}) {
|
||||
return (
|
||||
<section className={`space-y-4 ${className}`}>
|
||||
<div className={`flex items-end justify-between gap-4 ${tone === "bg-card border border-border shadow-sm" ? "border-b border-border pb-3" : ""}`}>
|
||||
<div className={`flex items-end justify-between gap-4 ${tone === "surface" ? "border-b border-border pb-3" : ""}`}>
|
||||
<div>
|
||||
{eyebrow ? <div className="mb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">{eyebrow}</div> : null}
|
||||
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-foreground">{title}</h2>
|
||||
|
||||
@@ -745,7 +745,7 @@ export const en: Record<string, string> = {
|
||||
"ops.newMeeting": "New meeting",
|
||||
"ops.newSprint": "New sprint",
|
||||
"ops.deleteDocument": "Delete document",
|
||||
"ops.deleteDocumentMsg": "This removes the document from the company knowledge bg-card border border-border shadow-sm.",
|
||||
"ops.deleteDocumentMsg": "This removes the document from the company knowledge base.",
|
||||
"ops.deleteKnowledgeEntry": "Delete knowledge entry",
|
||||
"ops.deleteKnowledgeMsg": "This removes the entry from the shared company knowledge base.",
|
||||
"ops.deleteMeeting": "Delete meeting",
|
||||
|
||||
Reference in new issue
Block a user