Skip to content

feat: allow owners to edit skill summary#1411

Open
SylvanXiao wants to merge 1 commit intoopenclaw:mainfrom
SylvanXiao:pr-to-openclaw
Open

feat: allow owners to edit skill summary#1411
SylvanXiao wants to merge 1 commit intoopenclaw:mainfrom
SylvanXiao:pr-to-openclaw

Conversation

@SylvanXiao
Copy link
Copy Markdown

make the summary editable

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 30, 2026

Someone is attempting to deploy a commit to the 0xBuns Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR adds the ability for skill owners and moderators to edit a skill's summary text directly from the skill detail page, without needing to republish the skill.

Key changes:

  • convex/skills.ts: New updateSummary mutation, gated to the skill's ownerUserId or a moderator.
  • src/components/SkillDetailPage.tsx: Summary editing state (isSummaryEditing, isSummarySubmitting, summary) is managed here and passed as props to SkillHeader.
  • src/components/SkillHeader.tsx: The static summary <p> is replaced with an inline edit form (textarea + Save/Cancel buttons) when editing. The edit pencil button is shown to any canManage user.
  • src/styles.css: New scoped styles for the edit button and form, including dark-mode variants.

Issues found:

  • The form is rendered inside a <p> element, which is invalid HTML — block-level elements like <form>, <textarea>, and <div> cannot be children of <p>. Browsers silently close the <p> early, causing layout breakage.
  • The edit button is shown to publisher members (via the canManage flag), but the updateSummary backend only authorises ownerUserId or moderators. Publisher members will see the button but receive an error on save.
  • No character limit is enforced on the summary on either the frontend or backend.

Confidence Score: 3/5

Not safe to merge yet — a broken DOM structure causes layout issues in all browsers, and publisher members are shown an edit button they cannot use.

Two P1 issues are present: (1) the <form> inside a <p> produces invalid HTML that browsers misparse, causing visible layout bugs; (2) the canManage frontend gate is broader than the backend auth check, giving publisher members a broken editing experience. These should be resolved before merging.

src/components/SkillHeader.tsx (invalid HTML nesting) and convex/skills.ts (authorization mismatch with the frontend).

Important Files Changed

Filename Overview
convex/skills.ts Adds updateSummary mutation; auth check is ownerUserId or moderator, which mismatches the canManage gate used in the UI (publisher members see the edit button but get rejected). No length limit on the summary string.
src/components/SkillDetailPage.tsx Wires up summary editing state and passes it down to SkillHeader; logic and state management look correct aside from the canManage/publisher-member mismatch inherited from the backend.
src/components/SkillHeader.tsx Renders the edit form inside a <p> element, which is invalid HTML; the <form> and its block children cause browsers to implicitly close the paragraph, breaking layout. Otherwise the UI flow is reasonable.
src/styles.css Adds focused, self-contained CSS for the edit button and form; dark-mode variants are included and styles are properly scoped.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/components/SkillHeader.tsx
Line: 214-263

Comment:
**`<form>` nested inside `<p>` is invalid HTML**

A `<form>` (flow content) cannot be a child of `<p>` (which only permits phrasing content). When browsers parse this, they implicitly close the `<p>` before the `<form>`, producing a broken DOM that causes layout issues — e.g. the form floats outside the intended container and `.section-subtitle` styles don't apply to the edit view.

Replace the outer `<p>` with a `<div>` (keeping the `section-subtitle` class) so both the static text and the form share the same valid parent:

```suggestion
              <div className="section-subtitle">
                {isSummaryEditing ? (
                  <form
                    onSubmit={(event) => {
                      event.preventDefault();
                      onSummarySubmit();
                    }}
                    className="summary-edit-form"
                  >
                    <textarea
                      className="search-input summary-textarea"
                      value={summary}
                      onChange={(event) => onSummaryChange(event.target.value)}
                      placeholder="Enter a brief summary..."
                      rows={2}
                    />
                    <div className="summary-edit-actions">
                      <button
                        className="btn"
                        type="submit"
                        disabled={isSummarySubmitting}
                      >
                        {isSummarySubmitting ? "Saving..." : "Save"}
                      </button>
                      <button
                        className="btn btn-ghost"
                        type="button"
                        onClick={onSummaryEditToggle}
                        disabled={isSummarySubmitting}
                      >
                        Cancel
                      </button>
                    </div>
                  </form>
                ) : (
                  <>
                    {skill.summary ?? "No summary provided."}
                    {canManage && (
                      <button
                        className="edit-summary-btn"
                        type="button"
                        onClick={onSummaryEditToggle}
                        title="Edit summary"
                      >

                      </button>
                    )}
                  </>
                )}
              </div>
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: convex/skills.ts
Line: 4799-4820

Comment:
**Publisher members can see "Edit summary" but are blocked by the backend**

The edit button is shown to anyone where `canManage` is true, which includes members of the publisher that owns the skill (via `myPublisherIds.has(skill.ownerPublisherId)`). However, `updateSummary` only permits `skill.ownerUserId === user._id` or a moderator — publisher members are rejected.

When a publisher member (who is not the `ownerUserId` and not a moderator) hits "Save", they will receive an error and see `window.alert("Failed to update summary. Please try again.")`.

Either extend the backend to accept publisher members, or restrict the edit button to `isOwner` only:

**Option A — extend backend to also allow publisher members:**
```ts
if (skill.ownerUserId !== user._id) {
  let isPublisherMember = false;
  if (skill.ownerPublisherId) {
    const membership = await ctx.db
      .query("publisherMembers")
      .withIndex("by_publisher_user", (q) =>
        q.eq("publisherId", skill.ownerPublisherId!).eq("userId", user._id),
      )
      .unique();
    isPublisherMember = membership !== null;
  }
  if (!isPublisherMember) {
    assertModerator(user);
  }
}
```

**Option B — gate the button behind `isOwner` instead of `canManage`** (simpler, matches the current backend check).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: convex/skills.ts
Line: 4800-4803

Comment:
**No maximum length validation on summary**

`v.string()` imposes no upper bound, so a client can submit an arbitrarily long string. Consider capping the summary (e.g. 500 characters) both in the validator and with a `maxLength` attribute on the textarea:

In the handler, a quick guard:
```ts
if (args.summary.length > 500) throw new Error("Summary must be 500 characters or fewer.");
```

And in the textarea:
```tsx
<textarea maxLength={500} ... />
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: allow owners to edit skill summary" | Re-trigger Greptile

Comment on lines +214 to +263
<p className="section-subtitle">
{isSummaryEditing ? (
<form
onSubmit={(event) => {
event.preventDefault();
onSummarySubmit();
}}
className="summary-edit-form"
>
<textarea
className="search-input summary-textarea"
value={summary}
onChange={(event) => onSummaryChange(event.target.value)}
placeholder="Enter a brief summary..."
rows={2}
/>
<div className="summary-edit-actions">
<button
className="btn"
type="submit"
disabled={isSummarySubmitting}
>
{isSummarySubmitting ? "Saving..." : "Save"}
</button>
<button
className="btn btn-ghost"
type="button"
onClick={onSummaryEditToggle}
disabled={isSummarySubmitting}
>
Cancel
</button>
</div>
</form>
) : (
<>
{skill.summary ?? "No summary provided."}
{canManage && (
<button
className="edit-summary-btn"
type="button"
onClick={onSummaryEditToggle}
title="Edit summary"
>
</button>
)}
</>
)}
</p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 <form> nested inside <p> is invalid HTML

A <form> (flow content) cannot be a child of <p> (which only permits phrasing content). When browsers parse this, they implicitly close the <p> before the <form>, producing a broken DOM that causes layout issues — e.g. the form floats outside the intended container and .section-subtitle styles don't apply to the edit view.

Replace the outer <p> with a <div> (keeping the section-subtitle class) so both the static text and the form share the same valid parent:

Suggested change
<p className="section-subtitle">
{isSummaryEditing ? (
<form
onSubmit={(event) => {
event.preventDefault();
onSummarySubmit();
}}
className="summary-edit-form"
>
<textarea
className="search-input summary-textarea"
value={summary}
onChange={(event) => onSummaryChange(event.target.value)}
placeholder="Enter a brief summary..."
rows={2}
/>
<div className="summary-edit-actions">
<button
className="btn"
type="submit"
disabled={isSummarySubmitting}
>
{isSummarySubmitting ? "Saving..." : "Save"}
</button>
<button
className="btn btn-ghost"
type="button"
onClick={onSummaryEditToggle}
disabled={isSummarySubmitting}
>
Cancel
</button>
</div>
</form>
) : (
<>
{skill.summary ?? "No summary provided."}
{canManage && (
<button
className="edit-summary-btn"
type="button"
onClick={onSummaryEditToggle}
title="Edit summary"
>
</button>
)}
</>
)}
</p>
<div className="section-subtitle">
{isSummaryEditing ? (
<form
onSubmit={(event) => {
event.preventDefault();
onSummarySubmit();
}}
className="summary-edit-form"
>
<textarea
className="search-input summary-textarea"
value={summary}
onChange={(event) => onSummaryChange(event.target.value)}
placeholder="Enter a brief summary..."
rows={2}
/>
<div className="summary-edit-actions">
<button
className="btn"
type="submit"
disabled={isSummarySubmitting}
>
{isSummarySubmitting ? "Saving..." : "Save"}
</button>
<button
className="btn btn-ghost"
type="button"
onClick={onSummaryEditToggle}
disabled={isSummarySubmitting}
>
Cancel
</button>
</div>
</form>
) : (
<>
{skill.summary ?? "No summary provided."}
{canManage && (
<button
className="edit-summary-btn"
type="button"
onClick={onSummaryEditToggle}
title="Edit summary"
>
</button>
)}
</>
)}
</div>
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/SkillHeader.tsx
Line: 214-263

Comment:
**`<form>` nested inside `<p>` is invalid HTML**

A `<form>` (flow content) cannot be a child of `<p>` (which only permits phrasing content). When browsers parse this, they implicitly close the `<p>` before the `<form>`, producing a broken DOM that causes layout issues — e.g. the form floats outside the intended container and `.section-subtitle` styles don't apply to the edit view.

Replace the outer `<p>` with a `<div>` (keeping the `section-subtitle` class) so both the static text and the form share the same valid parent:

```suggestion
              <div className="section-subtitle">
                {isSummaryEditing ? (
                  <form
                    onSubmit={(event) => {
                      event.preventDefault();
                      onSummarySubmit();
                    }}
                    className="summary-edit-form"
                  >
                    <textarea
                      className="search-input summary-textarea"
                      value={summary}
                      onChange={(event) => onSummaryChange(event.target.value)}
                      placeholder="Enter a brief summary..."
                      rows={2}
                    />
                    <div className="summary-edit-actions">
                      <button
                        className="btn"
                        type="submit"
                        disabled={isSummarySubmitting}
                      >
                        {isSummarySubmitting ? "Saving..." : "Save"}
                      </button>
                      <button
                        className="btn btn-ghost"
                        type="button"
                        onClick={onSummaryEditToggle}
                        disabled={isSummarySubmitting}
                      >
                        Cancel
                      </button>
                    </div>
                  </form>
                ) : (
                  <>
                    {skill.summary ?? "No summary provided."}
                    {canManage && (
                      <button
                        className="edit-summary-btn"
                        type="button"
                        onClick={onSummaryEditToggle}
                        title="Edit summary"
                      >

                      </button>
                    )}
                  </>
                )}
              </div>
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +4799 to +4820
export const updateSummary = mutation({
args: {
skillId: v.id("skills"),
summary: v.string(),
},
handler: async (ctx, args) => {
const { user } = await requireUser(ctx);
const skill = await ctx.db.get(args.skillId);
if (!skill) throw new Error("Skill not found");
if (skill.ownerUserId !== user._id) {
assertModerator(user);
}

const now = Date.now();
const patch: Partial<Doc<"skills">> = {
summary: args.summary,
updatedAt: now,
};

await ctx.db.patch(skill._id, patch);
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Publisher members can see "Edit summary" but are blocked by the backend

The edit button is shown to anyone where canManage is true, which includes members of the publisher that owns the skill (via myPublisherIds.has(skill.ownerPublisherId)). However, updateSummary only permits skill.ownerUserId === user._id or a moderator — publisher members are rejected.

When a publisher member (who is not the ownerUserId and not a moderator) hits "Save", they will receive an error and see window.alert("Failed to update summary. Please try again.").

Either extend the backend to accept publisher members, or restrict the edit button to isOwner only:

Option A — extend backend to also allow publisher members:

if (skill.ownerUserId !== user._id) {
  let isPublisherMember = false;
  if (skill.ownerPublisherId) {
    const membership = await ctx.db
      .query("publisherMembers")
      .withIndex("by_publisher_user", (q) =>
        q.eq("publisherId", skill.ownerPublisherId!).eq("userId", user._id),
      )
      .unique();
    isPublisherMember = membership !== null;
  }
  if (!isPublisherMember) {
    assertModerator(user);
  }
}

Option B — gate the button behind isOwner instead of canManage (simpler, matches the current backend check).

Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/skills.ts
Line: 4799-4820

Comment:
**Publisher members can see "Edit summary" but are blocked by the backend**

The edit button is shown to anyone where `canManage` is true, which includes members of the publisher that owns the skill (via `myPublisherIds.has(skill.ownerPublisherId)`). However, `updateSummary` only permits `skill.ownerUserId === user._id` or a moderator — publisher members are rejected.

When a publisher member (who is not the `ownerUserId` and not a moderator) hits "Save", they will receive an error and see `window.alert("Failed to update summary. Please try again.")`.

Either extend the backend to accept publisher members, or restrict the edit button to `isOwner` only:

**Option A — extend backend to also allow publisher members:**
```ts
if (skill.ownerUserId !== user._id) {
  let isPublisherMember = false;
  if (skill.ownerPublisherId) {
    const membership = await ctx.db
      .query("publisherMembers")
      .withIndex("by_publisher_user", (q) =>
        q.eq("publisherId", skill.ownerPublisherId!).eq("userId", user._id),
      )
      .unique();
    isPublisherMember = membership !== null;
  }
  if (!isPublisherMember) {
    assertModerator(user);
  }
}
```

**Option B — gate the button behind `isOwner` instead of `canManage`** (simpler, matches the current backend check).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +4800 to +4803
args: {
skillId: v.id("skills"),
summary: v.string(),
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No maximum length validation on summary

v.string() imposes no upper bound, so a client can submit an arbitrarily long string. Consider capping the summary (e.g. 500 characters) both in the validator and with a maxLength attribute on the textarea:

In the handler, a quick guard:

if (args.summary.length > 500) throw new Error("Summary must be 500 characters or fewer.");

And in the textarea:

<textarea maxLength={500} ... />
Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/skills.ts
Line: 4800-4803

Comment:
**No maximum length validation on summary**

`v.string()` imposes no upper bound, so a client can submit an arbitrarily long string. Consider capping the summary (e.g. 500 characters) both in the validator and with a `maxLength` attribute on the textarea:

In the handler, a quick guard:
```ts
if (args.summary.length > 500) throw new Error("Summary must be 500 characters or fewer.");
```

And in the textarea:
```tsx
<textarea maxLength={500} ... />
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d93742ef9a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +4808 to +4810
if (skill.ownerUserId !== user._id) {
assertModerator(user);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow publisher owners to update summaries

This mutation only authorizes skill.ownerUserId (or moderators), but org-owned skills are managed by publisher members in the UI (canManage becomes true from ownerPublisherId membership), so those legitimate owners can open the editor and then always hit an authorization failure on save. As written, the new summary-edit feature is broken for publisher-owned skills unless a moderator performs the edit.

Useful? React with 👍 / 👎.

Comment on lines +214 to +217
<p className="section-subtitle">
{isSummaryEditing ? (
<form
onSubmit={(event) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move summary edit form out of paragraph container

When edit mode is active, this renders a <form> as a child of <p className="section-subtitle">, which is invalid HTML content nesting. Browsers will auto-correct that structure, which can cause SSR hydration warnings and inconsistent header layout/behavior when toggling summary edit mode. Wrapping the editable block in a non-<p> container avoids this.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant