fix: address review feedback on skills and session compaction

This commit is contained in:
Dotta
2026-03-17 09:21:44 -05:00
parent 4323d4bbda
commit 4d9769c620
4 changed files with 61 additions and 10 deletions

View File

@@ -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<string, AdapterSessionManagement
claude_local: {
supportsSessionResume: true,
nativeContextManagement: "confirmed",
defaultSessionCompaction: DISABLED_SESSION_COMPACTION_POLICY,
defaultSessionCompaction: ADAPTER_MANAGED_SESSION_POLICY,
},
codex_local: {
supportsSessionResume: true,
nativeContextManagement: "confirmed",
defaultSessionCompaction: DISABLED_SESSION_COMPACTION_POLICY,
defaultSessionCompaction: ADAPTER_MANAGED_SESSION_POLICY,
},
cursor: {
supportsSessionResume: true,
@@ -171,4 +173,3 @@ export function hasSessionCompactionThresholds(policy: Pick<
>) {
return policy.maxSessionRuns > 0 || policy.maxRawInputTokens > 0 || policy.maxSessionAgeHours > 0;
}

View File

@@ -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<MarkdownEditorRef, MarkdownEditorProps>
// 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;

View File

@@ -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" && (
<SkillsTab
agent={agent}
companyId={resolvedCompanyId ?? undefined}
/>
)}
@@ -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 (
<div className="space-y-4">
@@ -1225,7 +1230,7 @@ function SkillsTab({ agent }: { agent: Agent; companyId?: string }) {
<h3 className="text-sm font-medium">Skills</h3>
<p className="text-sm text-muted-foreground">
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.
</p>
<p className="text-xs text-muted-foreground">
Agent: <span className="font-mono">{agent.name}</span>
@@ -1238,11 +1243,48 @@ function SkillsTab({ agent }: { agent: Agent; companyId?: string }) {
{instructionsPath ?? "No instructions file configured for this agent."}
</div>
</div>
<div className="space-y-2">
<div className="text-xs uppercase tracking-wide text-muted-foreground">
Available skills
</div>
{isLoading ? (
<p className="text-sm text-muted-foreground">Loading available skills</p>
) : error ? (
<p className="text-sm text-destructive">
{error instanceof Error ? error.message : "Failed to load available skills."}
</p>
) : skills.length === 0 ? (
<p className="text-sm text-muted-foreground">No local skills were found.</p>
) : (
<div className="space-y-2">
{skills.map((skill) => (
<SkillRow key={skill.name} skill={skill} />
))}
</div>
)}
</div>
</div>
</div>
);
}
function SkillRow({ skill }: { skill: AvailableSkill }) {
return (
<div className="rounded-md border border-border bg-muted/20 px-3 py-2 space-y-1.5">
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{skill.name}</span>
<Badge variant={skill.isPaperclipManaged ? "secondary" : "outline"}>
{skill.isPaperclipManaged ? "Paperclip" : "Local"}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{skill.description || "No description available."}
</p>
</div>
);
}
/* ---- Runs Tab ---- */
function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) {

View File

@@ -461,6 +461,9 @@ export function ProjectDetail() {
if (cachedTab === "configuration") {
return <Navigate to={`/projects/${canonicalProjectRef}/configuration`} replace />;
}
if (cachedTab === "budget") {
return <Navigate to={`/projects/${canonicalProjectRef}/budget`} replace />;
}
if (isProjectPluginTab(cachedTab)) {
return <Navigate to={`/projects/${canonicalProjectRef}?tab=${encodeURIComponent(cachedTab)}`} replace />;
}