Skip to content

Commit b0f5d6c

Browse files
committed
fix(agents): address React Doctor warnings on bundle editor
- Move parseBundleInput + ParsedBundle to utils/parseBundleInput.ts so the component file only exports components (only-export-components). - Sync the editable markdown draft from upstream content during render instead of in an effect, removing the chained/derived state-in-effect warnings (no-chain-state-updates, no-derived-state, no-event-handler). Behaviour is unchanged: the !editing guard still prevents a concurrent refetch from wiping an in-progress draft. - Add aria-label to the bundle-import and per-file editor textareas (control-has-associated-label). Generated-By: PostHog Code Task-Id: 3d7de9b7-6d82-4868-b49b-7bb5f3029b24
1 parent 2af9f1f commit b0f5d6c

4 files changed

Lines changed: 84 additions & 82 deletions

File tree

packages/ui/src/features/agent-applications/components/AgentBundleImportDialog.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { describe, expect, it } from "vitest";
2-
import { type ParsedBundle, parseBundleInput } from "./AgentBundleImportDialog";
2+
import {
3+
type ParsedBundle,
4+
parseBundleInput,
5+
} from "../utils/parseBundleInput";
36

47
type Expected =
58
| { ok: true; value: ParsedBundle }

packages/ui/src/features/agent-applications/components/AgentBundleImportDialog.tsx

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,7 @@ import { Button } from "@posthog/ui/primitives/Button";
33
import { Dialog, Flex, Text } from "@radix-ui/themes";
44
import { useMemo, useState } from "react";
55
import { useImportAgentDraftBundle } from "../hooks/useImportAgentDraftBundle";
6-
7-
const HEADER_RE = /^---\s*(.+?)\s*---\s*$/;
8-
const SKILL_PATH_RE = /^skills\/([a-z0-9-]+)\/SKILL\.md$/;
9-
10-
export interface ParsedBundle {
11-
agent_md?: string;
12-
skills?: { id: string; body: string }[];
13-
}
14-
15-
/**
16-
* Splits a fenced paste — alternating `--- <path> ---` headers and bodies —
17-
* into the import payload the server accepts. The format is deliberately
18-
* simple so the source files can be cat'd together as-is; only `agent.md`
19-
* and `skills/<id>/SKILL.md` are recognised.
20-
*/
21-
export function parseBundleInput(
22-
input: string,
23-
): { ok: true; value: ParsedBundle } | { ok: false; error: string } {
24-
const lines = input.replace(/\r\n/g, "\n").split("\n");
25-
const value: ParsedBundle = {};
26-
let current: { kind: "agent" } | { kind: "skill"; id: string } | null = null;
27-
let buf: string[] = [];
28-
29-
const flush = () => {
30-
if (!current) return;
31-
const content = buf.join("\n").replace(/^\n+|\n+$/g, "");
32-
if (current.kind === "agent") {
33-
value.agent_md = content;
34-
} else {
35-
if (!value.skills) value.skills = [];
36-
value.skills.push({ id: current.id, body: content });
37-
}
38-
};
39-
40-
for (const line of lines) {
41-
const m = HEADER_RE.exec(line);
42-
if (m) {
43-
flush();
44-
buf = [];
45-
const path = m[1];
46-
if (path === "agent.md") {
47-
current = { kind: "agent" };
48-
} else {
49-
const skill = SKILL_PATH_RE.exec(path);
50-
if (!skill) {
51-
return {
52-
ok: false,
53-
error: `Unsupported file path: "${path}". Use "agent.md" or "skills/<id>/SKILL.md".`,
54-
};
55-
}
56-
current = { kind: "skill", id: skill[1] };
57-
}
58-
continue;
59-
}
60-
if (current) buf.push(line);
61-
}
62-
flush();
63-
64-
if (value.agent_md === undefined && !value.skills?.length) {
65-
return {
66-
ok: false,
67-
error:
68-
"Nothing to import. Add at least one `--- agent.md ---` or `--- skills/<id>/SKILL.md ---` block.",
69-
};
70-
}
71-
return { ok: true, value };
72-
}
6+
import { parseBundleInput } from "../utils/parseBundleInput";
737

748
const SAMPLE = `--- agent.md ---
759
You are the growth review agent. …
@@ -147,6 +81,7 @@ export function AgentBundleImportDialog({
14781
skills are overwritten by id; new ids are added.
14882
</Dialog.Description>
14983
<textarea
84+
aria-label="Markdown bundle"
15085
value={input}
15186
onChange={(e) => setInput(e.currentTarget.value)}
15287
placeholder={SAMPLE}

packages/ui/src/features/agent-applications/components/AgentConfigurationPane.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,7 @@ import { Button } from "@posthog/ui/primitives/Button";
3939
import { CodeBlock } from "@posthog/ui/primitives/CodeBlock";
4040
import { toast } from "@posthog/ui/primitives/toast";
4141
import { Flex, Select, Switch, Text } from "@radix-ui/themes";
42-
import {
43-
type ReactNode,
44-
useCallback,
45-
useEffect,
46-
useMemo,
47-
useState,
48-
} from "react";
42+
import { type ReactNode, useCallback, useMemo, useState } from "react";
4943
import { useAgentApplication } from "../hooks/useAgentApplication";
5044
import { useAgentEnvKeys } from "../hooks/useAgentEnvKeys";
5145
import { useAgentRevision } from "../hooks/useAgentRevision";
@@ -2245,18 +2239,21 @@ function EditableMarkdownBody({
22452239
const initial = file?.content ?? "";
22462240
const [editing, setEditing] = useState(false);
22472241
const [draft, setDraft] = useState(initial);
2242+
const [syncedInitial, setSyncedInitial] = useState(initial);
22482243
const mutation = useUpdateAgentDraftBundleFile(
22492244
editable.idOrSlug,
22502245
editable.revisionId,
22512246
);
22522247

2253-
// Pull in upstream content changes (initial bundle load, post-save refetch),
2254-
// but only while the user isn't actively editing — otherwise an unrelated
2255-
// refetch (e.g. a concurrent bulk import) would silently wipe their draft.
2256-
// File-switch resets are handled by `key={editable.path}` at the call site.
2257-
useEffect(() => {
2258-
if (!editing) setDraft(initial);
2259-
}, [initial, editing]);
2248+
// Pull in upstream content changes (initial bundle load, post-save refetch)
2249+
// during render rather than via an effect, but only while the user isn't
2250+
// actively editing — otherwise an unrelated refetch (e.g. a concurrent bulk
2251+
// import) would silently wipe their draft. File-switch resets are handled by
2252+
// `key={editable.path}` at the call site.
2253+
if (initial !== syncedInitial && !editing) {
2254+
setSyncedInitial(initial);
2255+
setDraft(initial);
2256+
}
22602257

22612258
if (!editing) {
22622259
return (
@@ -2285,6 +2282,7 @@ function EditableMarkdownBody({
22852282
return (
22862283
<Flex direction="column" gap="2">
22872284
<textarea
2285+
aria-label={editable.path}
22882286
value={draft}
22892287
onChange={(e) => setDraft(e.currentTarget.value)}
22902288
disabled={mutation.isPending}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const HEADER_RE = /^---\s*(.+?)\s*---\s*$/;
2+
const SKILL_PATH_RE = /^skills\/([a-z0-9-]+)\/SKILL\.md$/;
3+
4+
export interface ParsedBundle {
5+
agent_md?: string;
6+
skills?: { id: string; body: string }[];
7+
}
8+
9+
/**
10+
* Splits a fenced paste — alternating `--- <path> ---` headers and bodies —
11+
* into the import payload the server accepts. The format is deliberately
12+
* simple so the source files can be cat'd together as-is; only `agent.md`
13+
* and `skills/<id>/SKILL.md` are recognised.
14+
*/
15+
export function parseBundleInput(
16+
input: string,
17+
): { ok: true; value: ParsedBundle } | { ok: false; error: string } {
18+
const lines = input.replace(/\r\n/g, "\n").split("\n");
19+
const value: ParsedBundle = {};
20+
let current: { kind: "agent" } | { kind: "skill"; id: string } | null = null;
21+
let buf: string[] = [];
22+
23+
const flush = () => {
24+
if (!current) return;
25+
const content = buf.join("\n").replace(/^\n+|\n+$/g, "");
26+
if (current.kind === "agent") {
27+
value.agent_md = content;
28+
} else {
29+
if (!value.skills) value.skills = [];
30+
value.skills.push({ id: current.id, body: content });
31+
}
32+
};
33+
34+
for (const line of lines) {
35+
const m = HEADER_RE.exec(line);
36+
if (m) {
37+
flush();
38+
buf = [];
39+
const path = m[1];
40+
if (path === "agent.md") {
41+
current = { kind: "agent" };
42+
} else {
43+
const skill = SKILL_PATH_RE.exec(path);
44+
if (!skill) {
45+
return {
46+
ok: false,
47+
error: `Unsupported file path: "${path}". Use "agent.md" or "skills/<id>/SKILL.md".`,
48+
};
49+
}
50+
current = { kind: "skill", id: skill[1] };
51+
}
52+
continue;
53+
}
54+
if (current) buf.push(line);
55+
}
56+
flush();
57+
58+
if (value.agent_md === undefined && !value.skills?.length) {
59+
return {
60+
ok: false,
61+
error:
62+
"Nothing to import. Add at least one `--- agent.md ---` or `--- skills/<id>/SKILL.md ---` block.",
63+
};
64+
}
65+
return { ok: true, value };
66+
}

0 commit comments

Comments
 (0)