mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50:38 +09:00
fix(ui): external adapter selection, config field placement, and transcript parser freshness
- Fix external adapters (hermes, droid) not auto-selected when navigating with ?adapterType= param — was using a stale module-level Set built before async adapter registration - Move SchemaConfigFields to render after thinking effort (same visual area as Claude's chrome toggle) instead of bottom of config section - Extract SelectField into its own component to fix React hooks order violation when schema fields change between renders - Add onAdapterChange() subscription in registry.ts so registerUIAdapter() notifies components when dynamic parsers load, fixing stale parser for old runs - Add parserTick to both RunTranscriptView and useLiveRunTranscripts to force recomputation on parser change
This commit is contained in:
parent
69a1593ff8
commit
47f3cdc1bb
13 changed files with 473 additions and 55 deletions
|
|
@ -68,6 +68,7 @@ export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePa
|
||||||
case "stderr":
|
case "stderr":
|
||||||
case "system":
|
case "system":
|
||||||
case "stdout":
|
case "stdout":
|
||||||
|
case "diff":
|
||||||
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
|
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
|
||||||
case "tool_call":
|
case "tool_call":
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -268,17 +268,21 @@ export interface ProviderQuotaResult {
|
||||||
export interface ConfigFieldOption {
|
export interface ConfigFieldOption {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
/** Optional group key for categorizing options (e.g. provider name) */
|
||||||
|
group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigFieldSchema {
|
export interface ConfigFieldSchema {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: "text" | "select" | "toggle" | "number" | "textarea";
|
type: "text" | "select" | "toggle" | "number" | "textarea" | "combobox";
|
||||||
options?: ConfigFieldOption[];
|
options?: ConfigFieldOption[];
|
||||||
default?: unknown;
|
default?: unknown;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
group?: string;
|
group?: string;
|
||||||
|
/** Optional metadata — not rendered, but available to custom UI logic */
|
||||||
|
meta?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdapterConfigSchema {
|
export interface AdapterConfigSchema {
|
||||||
|
|
@ -340,7 +344,8 @@ export type TranscriptEntry =
|
||||||
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
|
| { kind: "result"; ts: string; text: string; inputTokens: number; outputTokens: number; cachedTokens: number; costUsd: number; subtype: string; isError: boolean; errors: string[] }
|
||||||
| { kind: "stderr"; ts: string; text: string }
|
| { kind: "stderr"; ts: string; text: string }
|
||||||
| { kind: "system"; ts: string; text: string }
|
| { kind: "system"; ts: string; text: string }
|
||||||
| { kind: "stdout"; ts: string; text: string };
|
| { kind: "stdout"; ts: string; text: string }
|
||||||
|
| { kind: "diff"; ts: string; changeType: "add" | "remove" | "context" | "hunk" | "file_header" | "truncation"; text: string };
|
||||||
|
|
||||||
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
|
export type StdoutLineParser = (line: string, ts: string) => TranscriptEntry[];
|
||||||
|
|
||||||
|
|
|
||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
|
|
@ -503,9 +503,6 @@ importers:
|
||||||
express:
|
express:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
hermes-paperclip-adapter:
|
|
||||||
specifier: ^0.2.0
|
|
||||||
version: 0.2.0
|
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^28.1.0
|
specifier: ^28.1.0
|
||||||
version: 28.1.0(@noble/hashes@2.0.1)
|
version: 28.1.0(@noble/hashes@2.0.1)
|
||||||
|
|
@ -639,9 +636,6 @@ importers:
|
||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
hermes-paperclip-adapter:
|
|
||||||
specifier: ^0.2.0
|
|
||||||
version: 0.2.0
|
|
||||||
lexical:
|
lexical:
|
||||||
specifier: 0.35.0
|
specifier: 0.35.0
|
||||||
version: 0.35.0
|
version: 0.35.0
|
||||||
|
|
@ -2043,9 +2037,6 @@ packages:
|
||||||
'@open-draft/deferred-promise@2.2.0':
|
'@open-draft/deferred-promise@2.2.0':
|
||||||
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
|
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
|
||||||
|
|
||||||
'@paperclipai/adapter-utils@2026.325.0':
|
|
||||||
resolution: {integrity: sha512-YDVSAgjkeJ0PvxXDJVN9MZDX7oYRzidLtGHmGgRGd6gSk/bF2ygAKvND4FI1YxDc/cRLQjqAFCpCYaC/9wqIEA==}
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||||
|
|
||||||
|
|
@ -4471,10 +4462,6 @@ packages:
|
||||||
help-me@5.0.0:
|
help-me@5.0.0:
|
||||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||||
|
|
||||||
hermes-paperclip-adapter@0.2.0:
|
|
||||||
resolution: {integrity: sha512-6CP5vxfvY4jY9XJK5zu4ZUL9aB7HHNtEMk6q7m1Pu9Gzoby1Vx5VNmVqte3NUO+1cvVK9Arj1f67xLagWkbo5Q==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0:
|
html-encoding-sniffer@6.0.0:
|
||||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
@ -7743,8 +7730,6 @@ snapshots:
|
||||||
|
|
||||||
'@open-draft/deferred-promise@2.2.0': {}
|
'@open-draft/deferred-promise@2.2.0': {}
|
||||||
|
|
||||||
'@paperclipai/adapter-utils@2026.325.0': {}
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.3.1':
|
'@paralleldrive/cuid2@2.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
|
|
@ -10340,11 +10325,6 @@ snapshots:
|
||||||
|
|
||||||
help-me@5.0.0: {}
|
help-me@5.0.0: {}
|
||||||
|
|
||||||
hermes-paperclip-adapter@0.2.0:
|
|
||||||
dependencies:
|
|
||||||
'@paperclipai/adapter-utils': 2026.325.0
|
|
||||||
picocolors: 1.1.1
|
|
||||||
|
|
||||||
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
|
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
'@exodus/bytes': 1.15.0(@noble/hashes@2.0.1)
|
||||||
|
|
|
||||||
|
|
@ -212,8 +212,23 @@ export async function reloadExternalAdapter(
|
||||||
const packageDir = resolvePackageDir(record);
|
const packageDir = resolvePackageDir(record);
|
||||||
const entryPoint = resolvePackageEntryPoint(packageDir);
|
const entryPoint = resolvePackageEntryPoint(packageDir);
|
||||||
const modulePath = path.resolve(packageDir, entryPoint);
|
const modulePath = path.resolve(packageDir, entryPoint);
|
||||||
|
const fileUrl = `file://${modulePath}`;
|
||||||
|
|
||||||
const cacheBustUrl = `file://${modulePath}?t=${Date.now()}`;
|
// Bust ESM module cache so re-import loads fresh code from disk.
|
||||||
|
// Query-string trick (?t=...) works in Node; Bun may need the file:// URL
|
||||||
|
// to be evicted from its internal registry first.
|
||||||
|
try {
|
||||||
|
// @ts-expect-error -- Bun internal module cache
|
||||||
|
const bunCache = globalThis.Bun?.__moduleCache as Map<string, unknown> | undefined;
|
||||||
|
if (bunCache) {
|
||||||
|
bunCache.delete(fileUrl);
|
||||||
|
bunCache.delete(modulePath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore — query-string fallback still works in Node
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheBustUrl = `${fileUrl}?t=${Date.now()}`;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{ type, packageName: record.packageName, modulePath, cacheBustUrl },
|
{ type, packageName: record.packageName, modulePath, cacheBustUrl },
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export {
|
||||||
registerUIAdapter,
|
registerUIAdapter,
|
||||||
unregisterUIAdapter,
|
unregisterUIAdapter,
|
||||||
syncExternalAdapters,
|
syncExternalAdapters,
|
||||||
|
onAdapterChange,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
export { buildTranscript } from "./transcript";
|
export { buildTranscript } from "./transcript";
|
||||||
export type {
|
export type {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,18 @@ export function listKnownAdapterTypes(): string[] {
|
||||||
* Unknown types (external adapters) are always considered enabled.
|
* Unknown types (external adapters) are always considered enabled.
|
||||||
*/
|
*/
|
||||||
export function isEnabledAdapterType(type: string): boolean {
|
export function isEnabledAdapterType(type: string): boolean {
|
||||||
|
// Known external adapter — always valid
|
||||||
|
if (listUIAdapters().some((a) => a.type === type)) return true;
|
||||||
|
return !getAdapterDisplay(type).comingSoon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an adapter type is a valid choice for new agent creation.
|
||||||
|
* Includes all registered UI adapters (built-in + external) and
|
||||||
|
* any non-"coming soon" adapter from the display registry.
|
||||||
|
*/
|
||||||
|
export function isValidAdapterType(type: string): boolean {
|
||||||
|
if (listUIAdapters().some((a) => a.type === type)) return true;
|
||||||
return !getAdapterDisplay(type).comingSoon;
|
return !getAdapterDisplay(type).comingSoon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@ import { SchemaConfigFields, buildSchemaAdapterConfig } from "./schema-config-fi
|
||||||
const uiAdapters: UIAdapterModule[] = [];
|
const uiAdapters: UIAdapterModule[] = [];
|
||||||
const adaptersByType = new Map<string, UIAdapterModule>();
|
const adaptersByType = new Map<string, UIAdapterModule>();
|
||||||
|
|
||||||
|
// Subscriber list — components can register to be notified when adapters change
|
||||||
|
// (e.g., when a dynamic parser replaces a placeholder).
|
||||||
|
const adapterChangeListeners = new Set<() => void>();
|
||||||
|
|
||||||
|
/** Subscribe to adapter registry changes. Returns unsubscribe function. */
|
||||||
|
export function onAdapterChange(fn: () => void): () => void {
|
||||||
|
adapterChangeListeners.add(fn);
|
||||||
|
return () => adapterChangeListeners.delete(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyAdapterChange(): void {
|
||||||
|
for (const fn of adapterChangeListeners) fn();
|
||||||
|
}
|
||||||
|
|
||||||
function registerBuiltInUIAdapters() {
|
function registerBuiltInUIAdapters() {
|
||||||
for (const adapter of [
|
for (const adapter of [
|
||||||
claudeLocalUIAdapter,
|
claudeLocalUIAdapter,
|
||||||
|
|
@ -40,6 +54,7 @@ export function registerUIAdapter(adapter: UIAdapterModule): void {
|
||||||
uiAdapters.push(adapter);
|
uiAdapters.push(adapter);
|
||||||
}
|
}
|
||||||
adaptersByType.set(adapter.type, adapter);
|
adaptersByType.set(adapter.type, adapter);
|
||||||
|
notifyAdapterChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregisterUIAdapter(type: string): void {
|
export function unregisterUIAdapter(type: string): void {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
import type { AdapterConfigSchema, ConfigFieldSchema, CreateConfigValues } from "@paperclipai/adapter-utils";
|
import type { AdapterConfigSchema, ConfigFieldSchema, CreateConfigValues } from "@paperclipai/adapter-utils";
|
||||||
|
|
||||||
|
|
@ -10,15 +10,197 @@ import {
|
||||||
DraftTextarea,
|
DraftTextarea,
|
||||||
ToggleField,
|
ToggleField,
|
||||||
} from "../components/agent-config-primitives";
|
} from "../components/agent-config-primitives";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "../components/ui/popover";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
// ── Select field (extracted to keep hooks at component top level) ──────
|
||||||
|
function SelectField({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
options: Array<{ value: string; label: string }>;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const selectedOpt = options.find((o) => o.value === value);
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between">
|
||||||
|
<span className={!value ? "text-muted-foreground" : ""}>
|
||||||
|
{selectedOpt?.label ?? value ?? "Select..."}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-1" align="start">
|
||||||
|
{options.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
className={`flex items-center w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50 ${opt.value === value ? "bg-accent" : ""}`}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onChange(opt.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{opt.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
const inputClass =
|
const inputClass =
|
||||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||||
|
|
||||||
const selectClass =
|
|
||||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Schema cache (module-level, survives re-renders)
|
// Combobox: type-to-filter dropdown with free text fallback
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function ComboboxField({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
options: { label: string; value: string; group?: string }[];
|
||||||
|
onChange: (val: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Sync filter with external value when it changes (e.g. provider switch resets model)
|
||||||
|
useEffect(() => {
|
||||||
|
setFilter("");
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const filtered = options.filter((opt) => {
|
||||||
|
if (!filter) return true;
|
||||||
|
const q = filter.toLowerCase();
|
||||||
|
return (
|
||||||
|
opt.value.toLowerCase().includes(q) ||
|
||||||
|
opt.label.toLowerCase().includes(q) ||
|
||||||
|
(opt.group && opt.group.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOpt = options.find((o) => o.value === value);
|
||||||
|
const displayValue = filter || selectedOpt?.value || value || "";
|
||||||
|
|
||||||
|
// Group filtered options by `group` field if present
|
||||||
|
const grouped = new Map<string, typeof filtered>();
|
||||||
|
for (const opt of filtered) {
|
||||||
|
const g = opt.group ?? "";
|
||||||
|
if (!grouped.has(g)) grouped.set(g, []);
|
||||||
|
grouped.get(g)!.push(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = useCallback(
|
||||||
|
(val: string) => {
|
||||||
|
onChange(val);
|
||||||
|
setOpen(false);
|
||||||
|
setFilter("");
|
||||||
|
inputRef.current?.blur();
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
// If exactly one match, select it. Otherwise commit the typed value.
|
||||||
|
if (filtered.length === 1) {
|
||||||
|
select(filtered[0].value);
|
||||||
|
} else if (filter) {
|
||||||
|
select(filter);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
setOpen(false);
|
||||||
|
setFilter("");
|
||||||
|
} else if (e.key === "ArrowDown" && !open) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex items-center gap-0">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
className="flex-1 rounded-l-md border border-r-0 border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40 focus:z-10"
|
||||||
|
value={displayValue}
|
||||||
|
placeholder={placeholder ?? "Type or select..."}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFilter(e.target.value);
|
||||||
|
if (!open) setOpen(true);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
if (!open) setOpen(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
// Delay close to allow click on option to register
|
||||||
|
setTimeout(() => setOpen(false), 150);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<Popover open={open && filtered.length > 0} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button className="rounded-r-md border border-border px-2 py-1.5 hover:bg-accent/50 transition-colors">
|
||||||
|
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-1 max-h-60 overflow-y-auto"
|
||||||
|
style={{ minWidth: 280 }}
|
||||||
|
align="start"
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{Array.from(grouped.entries()).map(([group, opts]) => (
|
||||||
|
<div key={group || "_ungrouped"}>
|
||||||
|
{group && (
|
||||||
|
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||||
|
{group}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{opts.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
className={`flex items-center w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50 ${
|
||||||
|
opt.value === value ? "bg-accent" : ""
|
||||||
|
}`}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault(); // prevent input blur
|
||||||
|
select(opt.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="truncate">{opt.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filter && filtered.length === 0 && (
|
||||||
|
<div className="px-2 py-1.5 text-sm text-muted-foreground">
|
||||||
|
Use "{filter}" as custom value (press Enter)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SchemaConfigFields component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const schemaCache = new Map<string, AdapterConfigSchema | null>();
|
const schemaCache = new Map<string, AdapterConfigSchema | null>();
|
||||||
|
|
@ -146,14 +328,42 @@ export function SchemaConfigFields({
|
||||||
|
|
||||||
function writeValue(field: ConfigFieldSchema, value: unknown): void {
|
function writeValue(field: ConfigFieldSchema, value: unknown): void {
|
||||||
if (isCreate) {
|
if (isCreate) {
|
||||||
set?.({
|
const next = {
|
||||||
adapterSchemaValues: {
|
adapterSchemaValues: {
|
||||||
...values?.adapterSchemaValues,
|
...values?.adapterSchemaValues,
|
||||||
[field.key]: value,
|
[field.key]: value,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// When provider changes, auto-clear model if it's not in the new provider's list
|
||||||
|
if (field.key === "provider" && schema) {
|
||||||
|
const modelField = schema.fields.find((f) => f.key === "model");
|
||||||
|
if (modelField?.meta?.providerModels) {
|
||||||
|
const modelsByProvider = modelField.meta.providerModels as Record<string, string[]>;
|
||||||
|
const providerModels = modelsByProvider[String(value)] ?? [];
|
||||||
|
const currentModel = values?.adapterSchemaValues?.model;
|
||||||
|
if (currentModel && String(value) !== "auto" && !providerModels.includes(String(currentModel))) {
|
||||||
|
next.adapterSchemaValues.model = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set?.(next);
|
||||||
} else {
|
} else {
|
||||||
mark("adapterConfig", field.key, value);
|
mark("adapterConfig", field.key, value);
|
||||||
|
|
||||||
|
// Same logic for edit mode
|
||||||
|
if (field.key === "provider" && schema) {
|
||||||
|
const modelField = schema.fields.find((f) => f.key === "model");
|
||||||
|
if (modelField?.meta?.providerModels) {
|
||||||
|
const modelsByProvider = modelField.meta.providerModels as Record<string, string[]>;
|
||||||
|
const providerModels = modelsByProvider[String(value)] ?? [];
|
||||||
|
const currentModel = eff("adapterConfig", "model", "");
|
||||||
|
if (currentModel && String(value) !== "auto" && !providerModels.includes(String(currentModel))) {
|
||||||
|
mark("adapterConfig", "model", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,22 +371,18 @@ export function SchemaConfigFields({
|
||||||
<>
|
<>
|
||||||
{schema.fields.map((field) => {
|
{schema.fields.map((field) => {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case "select":
|
case "select": {
|
||||||
|
const currentVal = String(readValue(field) ?? "");
|
||||||
return (
|
return (
|
||||||
<Field key={field.key} label={field.label} hint={field.hint}>
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
<select
|
<SelectField
|
||||||
className={selectClass}
|
value={currentVal}
|
||||||
value={String(readValue(field) ?? "")}
|
options={field.options ?? []}
|
||||||
onChange={(e) => writeValue(field, e.target.value)}
|
onChange={(v) => writeValue(field, v)}
|
||||||
>
|
/>
|
||||||
{field.options?.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "toggle":
|
case "toggle":
|
||||||
return (
|
return (
|
||||||
|
|
@ -212,6 +418,48 @@ export function SchemaConfigFields({
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "combobox": {
|
||||||
|
const currentVal = String(readValue(field) ?? "");
|
||||||
|
// Dynamic options: if meta.providerModels exists, compute options
|
||||||
|
// based on the current provider value
|
||||||
|
let comboboxOptions = field.options ?? [];
|
||||||
|
if (field.meta?.providerModels) {
|
||||||
|
const providerVal = String(readValue(schema.fields.find((f) => f.key === "provider")!) ?? "auto");
|
||||||
|
const modelsByProvider = field.meta.providerModels as Record<string, string[]>;
|
||||||
|
if (providerVal === "auto") {
|
||||||
|
// Auto: show all models from all providers, grouped by provider
|
||||||
|
const providerLabel = schema.fields.find((f) => f.key === "provider");
|
||||||
|
const providerOptions = providerLabel?.options ?? [];
|
||||||
|
comboboxOptions = Object.entries(modelsByProvider).flatMap(([prov, models]) =>
|
||||||
|
models.map((m) => ({
|
||||||
|
label: m,
|
||||||
|
value: m,
|
||||||
|
group: providerOptions.find((p) => p.value === prov)?.label ?? prov,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const providerModels = modelsByProvider[providerVal] ?? [];
|
||||||
|
const providerLabel = schema.fields.find((f) => f.key === "provider");
|
||||||
|
const provName = providerLabel?.options?.find((p) => p.value === providerVal)?.label ?? providerVal;
|
||||||
|
comboboxOptions = providerModels.map((m) => ({
|
||||||
|
label: m,
|
||||||
|
value: m,
|
||||||
|
group: provName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
|
<ComboboxField
|
||||||
|
value={currentVal}
|
||||||
|
options={comboboxOptions}
|
||||||
|
onChange={(v) => writeValue(field, v || undefined)}
|
||||||
|
placeholder={field.hint}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -693,8 +693,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Adapter-specific fields */}
|
{/* Adapter-specific fields are rendered inside Permissions & Configuration */}
|
||||||
<uiAdapter.ConfigFields {...adapterFieldProps} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -816,6 +815,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||||
{adapterType === "claude_local" && (
|
{adapterType === "claude_local" && (
|
||||||
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
<ClaudeLocalAdvancedFields {...adapterFieldProps} />
|
||||||
)}
|
)}
|
||||||
|
<uiAdapter.ConfigFields {...adapterFieldProps} />
|
||||||
|
|
||||||
<Field label="Extra args (comma-separated)" hint={help.extraArgs}>
|
<Field label="Extra args (comma-separated)" hint={help.extraArgs}>
|
||||||
<DraftInput
|
<DraftInput
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
|
GitCompare,
|
||||||
TerminalSquare,
|
TerminalSquare,
|
||||||
User,
|
User,
|
||||||
Wrench,
|
Wrench,
|
||||||
|
|
@ -104,6 +105,16 @@ type TranscriptBlock =
|
||||||
tone: "info" | "warn" | "error" | "neutral";
|
tone: "info" | "warn" | "error" | "neutral";
|
||||||
text: string;
|
text: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "diff_group";
|
||||||
|
ts: string;
|
||||||
|
endTs?: string;
|
||||||
|
filePath?: string;
|
||||||
|
hunks: Array<{
|
||||||
|
changeType: "add" | "remove" | "context" | "hunk" | "file_header" | "truncation";
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
|
@ -568,6 +579,28 @@ export function normalizeTranscript(entries: TranscriptEntry[], streaming: boole
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Diff entries — accumulate into diff_group blocks ──────────
|
||||||
|
if (entry.kind === "diff") {
|
||||||
|
const prev = blocks[blocks.length - 1];
|
||||||
|
if (prev && prev.type === "diff_group") {
|
||||||
|
if (entry.changeType === "file_header") {
|
||||||
|
// New file in the same diff block — update filePath
|
||||||
|
prev.filePath = entry.text;
|
||||||
|
}
|
||||||
|
prev.hunks.push({ changeType: entry.changeType, text: entry.text });
|
||||||
|
prev.endTs = entry.ts;
|
||||||
|
} else {
|
||||||
|
blocks.push({
|
||||||
|
type: "diff_group",
|
||||||
|
ts: entry.ts,
|
||||||
|
endTs: entry.ts,
|
||||||
|
filePath: entry.changeType === "file_header" ? entry.text : undefined,
|
||||||
|
hunks: [{ changeType: entry.changeType, text: entry.text }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (previous?.type === "stdout") {
|
if (previous?.type === "stdout") {
|
||||||
previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`;
|
previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`;
|
||||||
previous.ts = entry.ts;
|
previous.ts = entry.ts;
|
||||||
|
|
@ -1093,6 +1126,103 @@ function TranscriptEventRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TranscriptDiffGroup({
|
||||||
|
block,
|
||||||
|
density,
|
||||||
|
}: {
|
||||||
|
block: Extract<TranscriptBlock, { type: "diff_group" }>;
|
||||||
|
density: TranscriptDensity;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const compact = density === "compact";
|
||||||
|
|
||||||
|
// Count add/remove lines (exclude context, hunk, file_header, truncation)
|
||||||
|
const addCount = block.hunks.filter((h) => h.changeType === "add").length;
|
||||||
|
const removeCount = block.hunks.filter((h) => h.changeType === "remove").length;
|
||||||
|
const hasChanges = addCount > 0 || removeCount > 0;
|
||||||
|
|
||||||
|
// Extract a short file name from the path
|
||||||
|
const shortFile = block.filePath
|
||||||
|
? block.filePath.split("/").pop() ?? block.filePath
|
||||||
|
: "diff";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-blue-500/20 bg-blue-500/[0.04] p-2">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="flex cursor-pointer items-center gap-2"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((v) => !v); } }}
|
||||||
|
>
|
||||||
|
<GitCompare className={compact ? "h-3.5 w-3.5" : "h-4 w-4"} />
|
||||||
|
<span className={cn("text-[11px] font-semibold uppercase tracking-[0.14em] text-blue-700 dark:text-blue-300")}>
|
||||||
|
{shortFile}
|
||||||
|
</span>
|
||||||
|
{hasChanges && (
|
||||||
|
<span className="text-[10px] tabular-nums">
|
||||||
|
<span className="text-emerald-600 dark:text-emerald-400">+{addCount}</span>
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-600 dark:text-red-400">-{removeCount}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<pre className={cn(
|
||||||
|
"mt-2 overflow-x-auto whitespace-pre-wrap break-words font-mono pl-5",
|
||||||
|
compact ? "text-[11px]" : "text-xs",
|
||||||
|
)}>
|
||||||
|
{block.hunks.map((hunk, i) => {
|
||||||
|
const key = `${i}-${hunk.changeType}`;
|
||||||
|
switch (hunk.changeType) {
|
||||||
|
case "remove":
|
||||||
|
return (
|
||||||
|
<span key={key} className="block bg-red-500/[0.10] text-red-700 dark:text-red-300 -mx-2 px-2">
|
||||||
|
<span className="select-none mr-2 text-red-500/60 dark:text-red-400/50">-</span>
|
||||||
|
{hunk.text}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "add":
|
||||||
|
return (
|
||||||
|
<span key={key} className="block bg-emerald-500/[0.10] text-emerald-700 dark:text-emerald-300 -mx-2 px-2">
|
||||||
|
<span className="select-none mr-2 text-emerald-500/60 dark:text-emerald-400/50">+</span>
|
||||||
|
{hunk.text}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "file_header":
|
||||||
|
return (
|
||||||
|
<span key={key} className="block font-semibold text-blue-600 dark:text-blue-300 mt-2 first:mt-0">
|
||||||
|
{hunk.text}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "truncation":
|
||||||
|
return (
|
||||||
|
<span key={key} className="block text-muted-foreground italic mt-1">
|
||||||
|
{hunk.text}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "context":
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<span key={key} className="block text-muted-foreground/70">
|
||||||
|
{" "}
|
||||||
|
{hunk.text}
|
||||||
|
{"\n"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TranscriptStderrGroup({
|
function TranscriptStderrGroup({
|
||||||
block,
|
block,
|
||||||
density,
|
density,
|
||||||
|
|
@ -1251,6 +1381,7 @@ export function RunTranscriptView({
|
||||||
{block.type === "tool" && <TranscriptToolCard block={block} density={density} />}
|
{block.type === "tool" && <TranscriptToolCard block={block} density={density} />}
|
||||||
{block.type === "command_group" && <TranscriptCommandGroup block={block} density={density} />}
|
{block.type === "command_group" && <TranscriptCommandGroup block={block} density={density} />}
|
||||||
{block.type === "tool_group" && <TranscriptToolGroup block={block} density={density} />}
|
{block.type === "tool_group" && <TranscriptToolGroup block={block} density={density} />}
|
||||||
|
{block.type === "diff_group" && <TranscriptDiffGroup block={block} density={density} />}
|
||||||
{block.type === "stderr_group" && <TranscriptStderrGroup block={block} density={density} />}
|
{block.type === "stderr_group" && <TranscriptStderrGroup block={block} density={density} />}
|
||||||
{block.type === "stdout" && (
|
{block.type === "stdout" && (
|
||||||
<TranscriptStdoutRow block={block} density={density} collapseByDefault={collapseStdout} />
|
<TranscriptStdoutRow block={block} density={density} collapseByDefault={collapseStdout} />
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||||
import type { LiveEvent } from "@paperclipai/shared";
|
import type { LiveEvent } from "@paperclipai/shared";
|
||||||
import { instanceSettingsApi } from "../../api/instanceSettings";
|
import { instanceSettingsApi } from "../../api/instanceSettings";
|
||||||
import { heartbeatsApi, type LiveRunForIssue } from "../../api/heartbeats";
|
import { heartbeatsApi, type LiveRunForIssue } from "../../api/heartbeats";
|
||||||
import { buildTranscript, getUIAdapter, type RunLogChunk, type TranscriptEntry } from "../../adapters";
|
import { buildTranscript, getUIAdapter, onAdapterChange, type RunLogChunk, type TranscriptEntry } from "../../adapters";
|
||||||
import { queryKeys } from "../../lib/queryKeys";
|
import { queryKeys } from "../../lib/queryKeys";
|
||||||
|
|
||||||
const LOG_POLL_INTERVAL_MS = 2000;
|
const LOG_POLL_INTERVAL_MS = 2000;
|
||||||
|
|
@ -68,6 +68,11 @@ export function useLiveRunTranscripts({
|
||||||
const seenChunkKeysRef = useRef(new Set<string>());
|
const seenChunkKeysRef = useRef(new Set<string>());
|
||||||
const pendingLogRowsByRunRef = useRef(new Map<string, string>());
|
const pendingLogRowsByRunRef = useRef(new Map<string, string>());
|
||||||
const logOffsetByRunRef = useRef(new Map<string, number>());
|
const logOffsetByRunRef = useRef(new Map<string, number>());
|
||||||
|
// Tick counter to force transcript recomputation when dynamic parser loads
|
||||||
|
const [parserTick, setParserTick] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
return onAdapterChange(() => setParserTick((t) => t + 1));
|
||||||
|
}, []);
|
||||||
const { data: generalSettings } = useQuery({
|
const { data: generalSettings } = useQuery({
|
||||||
queryKey: queryKeys.instance.generalSettings,
|
queryKey: queryKeys.instance.generalSettings,
|
||||||
queryFn: () => instanceSettingsApi.getGeneral(),
|
queryFn: () => instanceSettingsApi.getGeneral(),
|
||||||
|
|
@ -285,7 +290,7 @@ export function useLiveRunTranscripts({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}, [chunksByRun, generalSettings?.censorUsernameInLogs, runs]);
|
}, [chunksByRun, generalSettings?.censorUsernameInLogs, parserTick, runs]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transcriptByRun,
|
transcriptByRun,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import { PageTabBar } from "../components/PageTabBar";
|
||||||
import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives";
|
import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives";
|
||||||
import { MarkdownEditor } from "../components/MarkdownEditor";
|
import { MarkdownEditor } from "../components/MarkdownEditor";
|
||||||
import { assetsApi } from "../api/assets";
|
import { assetsApi } from "../api/assets";
|
||||||
import { getUIAdapter, buildTranscript } from "../adapters";
|
import { getUIAdapter, buildTranscript, onAdapterChange } from "../adapters";
|
||||||
import { StatusBadge } from "../components/StatusBadge";
|
import { StatusBadge } from "../components/StatusBadge";
|
||||||
import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors";
|
import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors";
|
||||||
import { MarkdownBody } from "../components/MarkdownBody";
|
import { MarkdownBody } from "../components/MarkdownBody";
|
||||||
|
|
@ -3762,10 +3762,20 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||||
return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs);
|
return redactPathValue(asRecord(evt?.payload ?? null), censorUsernameInLogs);
|
||||||
}, [censorUsernameInLogs, events]);
|
}, [censorUsernameInLogs, events]);
|
||||||
|
|
||||||
const adapter = useMemo(() => getUIAdapter(adapterType), [adapterType]);
|
// NOTE: adapter is NOT memoized because external adapters replace their
|
||||||
|
// parseStdoutLine asynchronously after dynamic parser loading. Memoizing
|
||||||
|
// on adapterType alone would stale the transcript with the fallback parser.
|
||||||
|
// We subscribe to adapter registry changes to force transcript recomputation.
|
||||||
|
const [parserTick, setParserTick] = useState(0);
|
||||||
|
const adapter = getUIAdapter(adapterType);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return onAdapterChange(() => setParserTick((t) => t + 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const transcript = useMemo(
|
const transcript = useMemo(
|
||||||
() => buildTranscript(logLines, adapter.parseStdoutLine, { censorUsernameInLogs }),
|
() => buildTranscript(logLines, adapter.parseStdoutLine, { censorUsernameInLogs }),
|
||||||
[adapter, censorUsernameInLogs, logLines],
|
[adapter, censorUsernameInLogs, logLines, parserTick],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { AgentConfigForm, type CreateConfigValues } from "../components/AgentCon
|
||||||
import { defaultCreateValues } from "../components/agent-config-defaults";
|
import { defaultCreateValues } from "../components/agent-config-defaults";
|
||||||
import { getUIAdapter, listUIAdapters } from "../adapters";
|
import { getUIAdapter, listUIAdapters } from "../adapters";
|
||||||
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
||||||
|
import { isValidAdapterType } from "../adapters/metadata";
|
||||||
import { ReportsToPicker } from "../components/ReportsToPicker";
|
import { ReportsToPicker } from "../components/ReportsToPicker";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||||
|
|
@ -29,10 +30,6 @@ import {
|
||||||
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
|
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
|
||||||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local";
|
import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local";
|
||||||
|
|
||||||
const SUPPORTED_ADVANCED_ADAPTER_TYPES = new Set<CreateConfigValues["adapterType"]>(
|
|
||||||
listUIAdapters().map((adapter) => adapter.type as CreateConfigValues["adapterType"]),
|
|
||||||
);
|
|
||||||
|
|
||||||
function createValuesForAdapterType(
|
function createValuesForAdapterType(
|
||||||
adapterType: CreateConfigValues["adapterType"],
|
adapterType: CreateConfigValues["adapterType"],
|
||||||
): CreateConfigValues {
|
): CreateConfigValues {
|
||||||
|
|
@ -114,9 +111,7 @@ export function NewAgent() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const requested = presetAdapterType;
|
const requested = presetAdapterType;
|
||||||
if (!requested) return;
|
if (!requested) return;
|
||||||
if (!SUPPORTED_ADVANCED_ADAPTER_TYPES.has(requested as CreateConfigValues["adapterType"])) {
|
if (!isValidAdapterType(requested)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
setConfigValues((prev) => {
|
setConfigValues((prev) => {
|
||||||
if (prev.adapterType === requested) return prev;
|
if (prev.adapterType === requested) return prev;
|
||||||
return createValuesForAdapterType(requested as CreateConfigValues["adapterType"]);
|
return createValuesForAdapterType(requested as CreateConfigValues["adapterType"]);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue