diff --git a/packages/adapter-utils/src/session-compaction.ts b/packages/adapter-utils/src/session-compaction.ts index 0807c926..308b54a3 100644 --- a/packages/adapter-utils/src/session-compaction.ts +++ b/packages/adapter-utils/src/session-compaction.ts @@ -27,7 +27,9 @@ const DEFAULT_SESSION_COMPACTION_POLICY: SessionCompactionPolicy = { maxSessionAgeHours: 72, }; -const DISABLED_SESSION_COMPACTION_POLICY: SessionCompactionPolicy = { +// Adapters with native context management still participate in session resume, +// but Paperclip should not rotate them using threshold-based compaction. +const ADAPTER_MANAGED_SESSION_POLICY: SessionCompactionPolicy = { enabled: true, maxSessionRuns: 0, maxRawInputTokens: 0, @@ -47,12 +49,12 @@ export const ADAPTER_SESSION_MANAGEMENT: Record) { return policy.maxSessionRuns > 0 || policy.maxRawInputTokens > 0 || policy.maxSessionAgeHours > 0; } - diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index a2e49d0a..29a57a3a 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -61,6 +61,10 @@ export interface MarkdownEditorRef { focus: () => void; } +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + /* ---- Mention detection helpers ---- */ interface MentionState { @@ -255,9 +259,10 @@ export const MarkdownEditor = forwardRef // so the cursor isn't stuck right next to the image. setTimeout(() => { const current = latestValueRef.current; + const escapedSrc = escapeRegExp(src); const updated = current.replace( - /!\[([^\]]*)\]\(([^)]+)\)(?!\n\n)/g, - "![$1]($2)\n\n", + new RegExp(`(!\\[[^\\]]*\\]\\(${escapedSrc}\\))(?!\\n\\n)`, "g"), + "$1\n\n", ); if (updated !== current) { latestValueRef.current = updated; diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 9faaf005..211f0e7f 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { agentsApi, type AgentKey, type ClaudeLoginResult } from "../api/agents"; +import { agentsApi, type AgentKey, type ClaudeLoginResult, type AvailableSkill } from "../api/agents"; import { budgetsApi } from "../api/budgets"; import { heartbeatsApi } from "../api/heartbeats"; import { ApiError } from "../api/client"; @@ -30,6 +30,7 @@ import { ScrollToBottom } from "../components/ScrollToBottom"; import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { Tabs } from "@/components/ui/tabs"; import { Popover, @@ -743,7 +744,6 @@ export function AgentDetail() { {activeView === "skills" && ( )} @@ -1213,11 +1213,16 @@ function ConfigurationTab({ ); } -function SkillsTab({ agent }: { agent: Agent; companyId?: string }) { +function SkillsTab({ agent }: { agent: Agent }) { const instructionsPath = typeof agent.adapterConfig?.instructionsFilePath === "string" && agent.adapterConfig.instructionsFilePath.trim().length > 0 ? agent.adapterConfig.instructionsFilePath : null; + const { data, isLoading, error } = useQuery({ + queryKey: queryKeys.skills.available, + queryFn: () => agentsApi.availableSkills(), + }); + const skills = data?.skills ?? []; return (
@@ -1225,7 +1230,7 @@ function SkillsTab({ agent }: { agent: Agent; companyId?: string }) {

Skills

Skills are reusable instruction bundles the agent can invoke from its local tool environment. - This view keeps the tab compile-safe and shows the current instructions file path while the broader skills listing work continues elsewhere in the tree. + This view shows the current instructions file and the skills currently visible to the local agent runtime.

Agent: {agent.name} @@ -1238,11 +1243,48 @@ function SkillsTab({ agent }: { agent: Agent; companyId?: string }) { {instructionsPath ?? "No instructions file configured for this agent."}

+ +
+
+ Available skills +
+ {isLoading ? ( +

Loading available skills…

+ ) : error ? ( +

+ {error instanceof Error ? error.message : "Failed to load available skills."} +

+ ) : skills.length === 0 ? ( +

No local skills were found.

+ ) : ( +
+ {skills.map((skill) => ( + + ))} +
+ )} +
); } +function SkillRow({ skill }: { skill: AvailableSkill }) { + return ( +
+
+ {skill.name} + + {skill.isPaperclipManaged ? "Paperclip" : "Local"} + +
+

+ {skill.description || "No description available."} +

+
+ ); +} + /* ---- Runs Tab ---- */ function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) { diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index ebb7cec8..8727e80b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -461,6 +461,9 @@ export function ProjectDetail() { if (cachedTab === "configuration") { return ; } + if (cachedTab === "budget") { + return ; + } if (isProjectPluginTab(cachedTab)) { return ; }