Skip to content

Commit 01c614d

Browse files
committed
fix(skills,tools): audit fixes — autocomplete boundary, resize clipping, error routing
Two real regressions introduced while simplifying the extracted editor: - The schema-param autocomplete's trigger was rewritten to match a trailing identifier, but the completion still split on separators. The two disagreed, so typing `data.ci` opened the menu and selecting replaced `data.ci` whole — eating the member-access prefix. Both now share one SCHEMA_PARAM_WORD regex. - The uncapped markdown field measured its height only on value change while always setting overflow-hidden, so any width change that re-wrapped lines clipped the tail with no scrollbar to reach it. Now re-measures via ResizeObserver. Also from the audit: - Generation writes bypass the code field's change handler, so an open autocomplete stayed over a disabled streaming editor; close it on busy. - Delete failures rendered in the Schema section's error slot on both custom tool surfaces; route them to a toast instead. - Skill create navigated away while still dirty, stranding the unsaved-changes guard's history sentinel so Back landed on an empty create form. - The Description field on skill create never received its error border. - Drop a double-applied opacity-50 (the editor already dims when disabled), a dead try/catch around a non-throwing call that also shadowed the error prop, and a stale reference to a /tools page that does not exist. - Docs still described the removed GitHub-URL import and the old Add Skill dialog; rewrite for the create page and file/paste import.
1 parent 11120d1 commit 01c614d

8 files changed

Lines changed: 59 additions & 41 deletions

File tree

apps/docs/content/docs/en/agents/skills.mdx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ Skills live on the **Integrations** page: click **Integrations** in the workspac
2222

2323
![The Skills tab on the Integrations page](/static/skills/skills-tab.png)
2424

25-
Click **+ Add to Sim** to open the **Add Skill** dialog. The **Create** tab takes three fields:
26-
27-
![The Add Skill dialog, Create tab](/static/skills/add-skill-create.png)
25+
Click **+ Add to Sim** to open the skill create page, which takes three fields:
2826

2927
| Field | Description |
3028
|-------|-------------|
@@ -38,13 +36,10 @@ Click **+ Add to Sim** to open the **Add Skill** dialog. The **Create** tab take
3836

3937
### Importing skills
4038

41-
The **Import** tab brings in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format, three ways:
42-
43-
![The Add Skill dialog, Import tab](/static/skills/add-skill-import.png)
39+
Bring in an existing skill in the open [SKILL.md](https://agentskills.io/specification) format two ways:
4440

45-
- **Upload a file** — a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`.
46-
- **Import from GitHub** — paste a GitHub URL to a `SKILL.md` and click **Fetch**.
47-
- **Paste content** — paste the `SKILL.md` directly. The frontmatter carries the `name` and `description`; the markdown body is the content.
41+
- **Import** — the **Import** action on the create page takes a `.md` file with YAML frontmatter, or a `.zip` containing a `SKILL.md`.
42+
- **Paste content** — paste the `SKILL.md` straight into **Content**. The frontmatter carries the `name` and `description`; the markdown body is the content.
4843

4944
Integration pages suggest **curated skills** for their service — open one (HubSpot, for example) and add a suggested skill with one click.
5045

@@ -174,7 +169,7 @@ import { FAQ } from '@/components/ui/faq'
174169
{ question: "When should I use skills vs. agent instructions?", answer: "Use skills for knowledge that applies across multiple workflows or changes frequently. Skills are reusable packages that can be attached to any agent. Use agent instructions for task-specific context that is unique to a single agent and workflow. If you find yourself copying the same instructions into multiple agents, that content should be a skill instead." },
175170
{ question: "Can permission groups disable skills for certain users?", answer: "Yes. On Enterprise-entitled workspaces, any workspace admin can create a permission group with the disableSkills option enabled. When a user is assigned to such a group in a workspace, the skills dropdown in agent blocks is disabled and they cannot add or use skills in workflows belonging to that workspace." },
176171
{ question: "What is the recommended maximum length for skill content?", answer: "Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills. Shorter, focused skills are more effective because the agent can load exactly what it needs. A broad skill with too much content can overwhelm the agent and reduce the quality of its responses." },
177-
{ question: "Where do I create and manage skills?", answer: "Click Integrations in the workspace sidebar and switch to the Skills tab. Add Skill creates one from a name (kebab-case, max 64 characters), description (max 1024 characters), and markdown content — or imports an existing SKILL.md from a file, a GitHub URL, or pasted content. Click any existing skill to open its detail page, where you edit, share, and delete it." },
172+
{ question: "Where do I create and manage skills?", answer: "Click Integrations in the workspace sidebar and switch to the Skills tab. Add to Sim opens a create page taking a name (kebab-case, max 64 characters), description (max 1024 characters), and markdown content — or imports an existing SKILL.md from a file or pasted content. Click any existing skill to open its detail page, where you edit, share, and delete it." },
178173
{ question: "Who can edit a skill, and who can use it?", answer: "Everyone in the workspace — including people who join later — sees and uses every skill without being added to anything. Each skill has an editors list: editors and workspace admins (who are always editors, automatically) can edit, delete, and share the skill, and whoever creates a skill becomes an editor. The editors list never affects who can use or run a skill: a workflow that references a skill always executes it." },
179174
{ question: "Can I use skills in Chat?", answer: "Yes. Type / in the Chat message box to open the skills menu and pick a skill — for example /format-markdown. The tag loads the skill's full instructions into the conversation, so Sim follows them for that request without having to decide on its own to load the skill." },
180175
]} />

apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-code-field.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useEffect, useRef, useState } from 'react'
44
import {
55
ChipTag,
66
CODE_LINE_HEIGHT_PX,
7-
cn,
87
Popover,
98
PopoverAnchor,
109
PopoverContent,
@@ -50,14 +49,17 @@ interface TriggerState {
5049
searchTerm: string
5150
}
5251

52+
/** Trailing identifier under the caret — the unit both the trigger and the completion act on. */
53+
const SCHEMA_PARAM_WORD = /[a-zA-Z_]\w*$/
54+
5355
function checkSchemaParamTrigger(
5456
text: string,
5557
cursorPos: number,
5658
parameters: SchemaParameter[]
5759
): TriggerState {
5860
if (parameters.length === 0) return { show: false, searchTerm: '' }
5961

60-
const currentWord = text.slice(0, cursorPos).match(/[a-zA-Z_]\w*$/)?.[0] ?? ''
62+
const currentWord = text.slice(0, cursorPos).match(SCHEMA_PARAM_WORD)?.[0] ?? ''
6163
if (!currentWord) return { show: false, searchTerm: '' }
6264

6365
const lower = currentWord.toLowerCase()
@@ -96,6 +98,14 @@ export function CustomToolCodeField({
9698
const busy = generation.isLoading || generation.isStreaming
9799
const resolvedMinHeight = schemaParameters.length > 0 ? '380px' : '420px'
98100

101+
/** Generation writes bypass `handleChange`, so close any open menu here instead. */
102+
useEffect(() => {
103+
if (!busy) return
104+
setShowEnvVars(false)
105+
setShowTags(false)
106+
setShowSchemaParams(false)
107+
}, [busy])
108+
99109
useEffect(() => {
100110
if (!showSchemaParams || schemaParamSelectedIndex < 0) return
101111
const element = schemaParamItemRefs.current?.get(schemaParamSelectedIndex)
@@ -106,8 +116,9 @@ export function CustomToolCodeField({
106116
onChange(newValue)
107117
if (busy) return
108118

109-
const textarea = codeEditorRef.current?.querySelector('textarea')
110-
if (!textarea) return
119+
const container = codeEditorRef.current
120+
const textarea = container?.querySelector('textarea')
121+
if (!container || !textarea) return
111122

112123
const pos = textarea.selectionStart
113124
setCursorPosition(pos)
@@ -117,17 +128,11 @@ export function CustomToolCodeField({
117128
const currentLine = lines.length
118129
const currentCol = lines[lines.length - 1].length
119130

120-
try {
121-
if (codeEditorRef.current) {
122-
const editorRect = codeEditorRef.current.getBoundingClientRect()
123-
setDropdownPosition({
124-
top: currentLine * CODE_LINE_HEIGHT_PX + 5,
125-
left: Math.min(currentCol * 8, editorRect.width - 260),
126-
})
127-
}
128-
} catch (error) {
129-
logger.error('Error calculating cursor position:', { error })
130-
}
131+
const editorRect = container.getBoundingClientRect()
132+
setDropdownPosition({
133+
top: currentLine * CODE_LINE_HEIGHT_PX + 5,
134+
left: Math.min(currentCol * 8, editorRect.width - 260),
135+
})
131136

132137
const envVarTrigger = checkEnvVarTrigger(newValue, pos)
133138
setShowEnvVars(envVarTrigger.show)
@@ -158,9 +163,10 @@ export function CustomToolCodeField({
158163
const beforeCursor = value.substring(0, pos)
159164
const afterCursor = value.substring(pos)
160165

161-
const words = beforeCursor.split(/[\s=();,{}[\]]+/)
162-
const currentWord = words[words.length - 1] || ''
163-
const wordStart = beforeCursor.lastIndexOf(currentWord)
166+
// Must match checkSchemaParamTrigger's boundary exactly — a looser split
167+
// here would replace text the trigger never matched (e.g. eat `data.`).
168+
const currentWord = beforeCursor.match(SCHEMA_PARAM_WORD)?.[0] ?? ''
169+
const wordStart = pos - currentWord.length
164170

165171
onChange(beforeCursor.substring(0, wordStart) + paramName + afterCursor)
166172
setShowSchemaParams(false)
@@ -252,7 +258,6 @@ export function CustomToolCodeField({
252258
placeholder={CODE_PLACEHOLDER}
253259
minHeight={resolvedMinHeight}
254260
error={error && !generation.isStreaming}
255-
className={cn(busy && 'cursor-not-allowed opacity-50')}
256261
highlightVariables={true}
257262
disabled={busy}
258263
onKeyDown={handleKeyDown}

apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema-field.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client'
22

3-
import { cn } from '@sim/emcn'
43
import { SCHEMA_PLACEHOLDER } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema'
54
import type { useSchemaGeneration } from '@/app/workspace/[workspaceId]/components/custom-tool-editor/use-custom-tool-generation'
65
import { CodeEditor } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/code-editor/code-editor'
@@ -33,7 +32,6 @@ export function CustomToolSchemaField({
3332
placeholder={SCHEMA_PLACEHOLDER}
3433
minHeight='420px'
3534
error={error}
36-
className={cn(busy && 'cursor-not-allowed opacity-50')}
3735
disabled={busy}
3836
/>
3937
)

apps/sim/app/workspace/[workspaceId]/components/custom-tool-editor/custom-tool-schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Shared parsing/validation for a custom tool's OpenAI function-calling JSON
3-
* schema. Used by every custom-tool editing surface (the canvas modal and the
4-
* `/tools` pages) so they agree on what a valid schema is.
3+
* schema. Used by both custom-tool editing surfaces (the canvas modal and the
4+
* Settings > Custom Tools detail page) so they agree on what a valid schema is.
55
*/
66

77
export interface SchemaParameter {

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,19 @@ function RawMarkdownField({
238238
if (!autoGrow) return
239239
const el = textareaRef.current
240240
if (!el) return
241-
el.style.height = 'auto'
242-
el.style.height = `${Math.max(el.scrollHeight, minHeight)}px`
241+
242+
const measure = () => {
243+
el.style.height = 'auto'
244+
el.style.height = `${Math.max(el.scrollHeight, minHeight)}px`
245+
}
246+
measure()
247+
248+
// The box is `overflow-hidden` while uncapped, so a width change that
249+
// re-wraps lines without touching `value` would clip the tail with no
250+
// scrollbar to reach it — re-measure whenever the element resizes.
251+
const observer = new ResizeObserver(measure)
252+
observer.observe(el)
253+
return () => observer.disconnect()
243254
}, [autoGrow, value, minHeight])
244255

245256
return (

apps/sim/app/workspace/[workspaceId]/settings/components/custom-tools/components/custom-tool-detail/custom-tool-detail.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useMemo, useState } from 'react'
4-
import { ChipConfirmModal } from '@sim/emcn'
4+
import { ChipConfirmModal, toast } from '@sim/emcn'
55
import { ArrowLeft } from '@sim/emcn/icons'
66
import { createLogger } from '@sim/logger'
77
import { getErrorMessage } from '@sim/utils/errors'
@@ -176,7 +176,9 @@ export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetail
176176
onBack()
177177
} catch (error) {
178178
logger.error('Failed to delete custom tool', error)
179-
setSchemaError(getErrorMessage(error, 'Failed to delete custom tool'))
179+
toast.error("Couldn't delete tool", {
180+
description: getErrorMessage(error, 'Please try again in a moment.'),
181+
})
180182
}
181183
}
182184

apps/sim/app/workspace/[workspaceId]/skills/new/skill-create.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ export function SkillCreate({ workspaceId }: SkillCreateProps) {
7373
const [contentSeed, setContentSeed] = useState(0)
7474
const [errors, setErrors] = useState<FieldErrors>({})
7575

76-
const isDirty = !!nameDraft.trim() || !!descriptionDraft.trim() || !!contentDraft.trim()
76+
// Drops on success so the guard pops its history sentinel before we navigate —
77+
// otherwise Back from the new skill lands on a stale, empty create form.
78+
const isDirty =
79+
!createSkill.isSuccess &&
80+
(!!nameDraft.trim() || !!descriptionDraft.trim() || !!contentDraft.trim())
7781

7882
const guard = useUnsavedChangesGuard({ isDirty, backHref: skillsHref })
7983

@@ -195,6 +199,7 @@ export function SkillCreate({ workspaceId }: SkillCreateProps) {
195199
autoComplete='off'
196200
data-lpignore='true'
197201
disabled={createSkill.isPending}
202+
error={!!errors.description}
198203
/>
199204
{errors.description && (
200205
<p className='mt-[9px] text-[var(--text-error)] text-caption'>{errors.description}</p>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ChipModalFooter,
99
ChipModalHeader,
1010
ChipModalTabs,
11+
toast,
1112
} from '@sim/emcn'
1213
import { createLogger } from '@sim/logger'
1314
import { getErrorMessage } from '@sim/utils/errors'
@@ -310,9 +311,10 @@ export function CustomToolModal({
310311
handleClose()
311312
} catch (error) {
312313
logger.error('Error deleting custom tool:', error)
313-
const errorMessage = getErrorMessage(error, 'Failed to delete custom tool')
314-
setSchemaError(`${errorMessage}. Please try again.`)
315-
setActiveSection('schema')
314+
// A delete failure is not a schema problem — keep it out of the Schema slot.
315+
toast.error("Couldn't delete tool", {
316+
description: getErrorMessage(error, 'Please try again in a moment.'),
317+
})
316318
setShowDeleteConfirm(false)
317319
}
318320
}

0 commit comments

Comments
 (0)