Skip to content

Commit abe7057

Browse files
committed
feat(tools): read-only tool detail, create lands on the new tool, drop dead wand prompt API
- Viewers without edit rights could not open a custom tool at all, while the equivalent skill and custom-block surfaces both offer a read-only view. The detail page now takes `readOnly`: editors inert, no Save/Discard/Delete, no Generate. Creating still requires edit rights. - Creating a tool bounced back to the list while creating a skill lands on the new skill. Tools now do the same. The upsert returns the workspace's whole tool list (newest first) rather than just the new row, so the id is matched by title instead of by index — the same trap that produced the skill-create navigation bug. - Remove `openPrompt`/`closePrompt` from useWand. `closePrompt`'s last callers went away with the custom-tool-modal extraction and `openPrompt` had none before it; nothing reads `isPromptVisible` any more either.
1 parent 01c614d commit abe7057

5 files changed

Lines changed: 59 additions & 48 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ interface CustomToolCodeFieldProps {
4242
* tag autocomplete is not offered.
4343
*/
4444
blockId?: string
45+
/** Renders the editor inert for viewers without edit rights. */
46+
disabled?: boolean
4547
}
4648

4749
interface TriggerState {
@@ -81,6 +83,7 @@ export function CustomToolCodeField({
8183
schemaParameters,
8284
workspaceId,
8385
blockId,
86+
disabled = false,
8487
}: CustomToolCodeFieldProps) {
8588
const codeEditorRef = useRef<HTMLDivElement>(null)
8689
const schemaParamItemRefs = useRef<Map<number, HTMLElement> | null>(null)
@@ -95,7 +98,7 @@ export function CustomToolCodeField({
9598
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
9699
const [schemaParamSelectedIndex, setSchemaParamSelectedIndex] = useState(0)
97100

98-
const busy = generation.isLoading || generation.isStreaming
101+
const busy = disabled || generation.isLoading || generation.isStreaming
99102
const resolvedMinHeight = schemaParameters.length > 0 ? '380px' : '420px'
100103

101104
/** Generation writes bypass `handleChange`, so close any open menu here instead. */

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface CustomToolSchemaFieldProps {
99
onChange: (value: string) => void
1010
error: boolean
1111
generation: ReturnType<typeof useSchemaGeneration>
12+
/** Renders the editor inert for viewers without edit rights. */
13+
disabled?: boolean
1214
}
1315

1416
/**
@@ -21,8 +23,9 @@ export function CustomToolSchemaField({
2123
onChange,
2224
error,
2325
generation,
26+
disabled = false,
2427
}: CustomToolSchemaFieldProps) {
25-
const busy = generation.isLoading || generation.isStreaming
28+
const busy = disabled || generation.isLoading || generation.isStreaming
2629

2730
return (
2831
<CodeEditor

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ interface CustomToolDetailProps {
3737
workspaceId: string
3838
/** `null` on the create flow, which starts from an empty draft. */
3939
tool: CustomToolDefinition | null
40+
/** Viewers without edit rights get the same page with every control inert. */
41+
readOnly?: boolean
4042
onBack: () => void
43+
/** Lands the caller on the tool it just created, matching the skill create flow. */
44+
onCreated?: (toolId: string) => void
4145
}
4246

4347
/**
@@ -46,7 +50,13 @@ interface CustomToolDetailProps {
4650
* stacked (no tabs — the page has room for both). Uses the same fields as the
4751
* canvas modal so the two surfaces never drift.
4852
*/
49-
export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetailProps) {
53+
export function CustomToolDetail({
54+
workspaceId,
55+
tool,
56+
readOnly = false,
57+
onBack,
58+
onCreated,
59+
}: CustomToolDetailProps) {
5060
const isEditing = !!tool
5161

5262
const createTool = useCreateCustomTool()
@@ -153,11 +163,15 @@ export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetail
153163
setSeededSchema(jsonSchema)
154164
setSeededCode(functionCode)
155165
} else {
156-
await createTool.mutateAsync({
166+
const created = await createTool.mutateAsync({
157167
workspaceId,
158168
tool: { title, schema, code: functionCode },
159169
})
160-
onBack()
170+
// The upsert responds with the workspace's whole tool list (newest
171+
// first), not just the new row — match by title rather than index.
172+
const createdId = created.find((t) => t.title === title)?.id
173+
if (createdId) onCreated?.(createdId)
174+
else onBack()
161175
}
162176
} catch (error) {
163177
logger.error('Failed to save custom tool', error)
@@ -209,16 +223,18 @@ export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetail
209223
'Define the JSON schema your agents call, and the code that runs.'
210224
}
211225
actions={[
212-
...(isEditing
213-
? saveDiscardActions({
214-
dirty,
215-
saving,
216-
onSave: handleSave,
217-
onDiscard: handleDiscard,
218-
saveDisabled: !isSchemaValid || streaming,
219-
})
220-
: createToolActions),
221-
...(tool
226+
...(readOnly
227+
? []
228+
: isEditing
229+
? saveDiscardActions({
230+
dirty,
231+
saving,
232+
onSave: handleSave,
233+
onDiscard: handleDiscard,
234+
saveDisabled: !isSchemaValid || streaming,
235+
})
236+
: createToolActions),
237+
...(tool && !readOnly
222238
? [
223239
{
224240
text: deleteTool.isPending ? 'Deleting...' : 'Delete',
@@ -237,11 +253,13 @@ export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetail
237253
schemaError ? <FieldErrorText>{schemaError}</FieldErrorText> : undefined
238254
}
239255
action={
240-
<GeneratePromptControl
241-
isLoading={schemaGeneration.isLoading}
242-
isStreaming={schemaGeneration.isStreaming}
243-
onSubmit={(prompt) => schemaGeneration.generateStream({ prompt })}
244-
/>
256+
readOnly ? undefined : (
257+
<GeneratePromptControl
258+
isLoading={schemaGeneration.isLoading}
259+
isStreaming={schemaGeneration.isStreaming}
260+
onSubmit={(prompt) => schemaGeneration.generateStream({ prompt })}
261+
/>
262+
)
245263
}
246264
>
247265
<CustomToolSchemaField
@@ -259,11 +277,13 @@ export function CustomToolDetail({ workspaceId, tool, onBack }: CustomToolDetail
259277
label='Code'
260278
headerAccessory={codeError ? <FieldErrorText>{codeError}</FieldErrorText> : undefined}
261279
action={
262-
<GeneratePromptControl
263-
isLoading={codeGeneration.isLoading}
264-
isStreaming={codeGeneration.isStreaming}
265-
onSubmit={(prompt) => codeGeneration.generateStream({ prompt })}
266-
/>
280+
readOnly ? undefined : (
281+
<GeneratePromptControl
282+
isLoading={codeGeneration.isLoading}
283+
isStreaming={codeGeneration.isStreaming}
284+
onSubmit={(prompt) => codeGeneration.generateStream({ prompt })}
285+
/>
286+
)
267287
}
268288
>
269289
<CustomToolCodeField

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

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

33
import { useState } from 'react'
4-
import { cn } from '@sim/emcn'
54
import { getErrorMessage } from '@sim/utils/errors'
65
import { ArrowRight, Plus, Wrench } from 'lucide-react'
76
import { useParams } from 'next/navigation'
@@ -76,13 +75,18 @@ export function CustomTools() {
7675
*/
7776
if (selectedToolId !== null && (isLoading || workspacePermissions.isLoading)) return null
7877

79-
if (canEdit && (isCreating || selectedTool)) {
78+
if ((isCreating && canEdit) || selectedTool) {
8079
return (
8180
<CustomToolDetail
8281
key={isCreating ? 'new' : selectedTool?.id}
8382
workspaceId={workspaceId}
8483
tool={isCreating ? null : (selectedTool ?? null)}
84+
readOnly={!canEdit}
8585
onBack={closeDetail}
86+
onCreated={(toolId) => {
87+
setIsCreating(false)
88+
void setSelectedToolId(toolId)
89+
}}
8690
/>
8791
)
8892
}
@@ -114,19 +118,13 @@ export function CustomTools() {
114118
key={tool.id}
115119
type='button'
116120
onClick={() => void setSelectedToolId(tool.id)}
117-
className={cn(
118-
'w-full rounded-lg p-2 text-left transition-colors',
119-
canEdit ? 'cursor-pointer hover-hover:bg-[var(--surface-active)]' : 'cursor-default'
120-
)}
121-
disabled={!canEdit}
121+
className='w-full cursor-pointer rounded-lg p-2 text-left transition-colors hover-hover:bg-[var(--surface-active)]'
122122
>
123123
<SettingsResourceRow
124124
icon={<Wrench />}
125125
title={tool.title || 'Unnamed Tool'}
126126
description={tool.schema?.function?.description || undefined}
127-
trailing={
128-
canEdit ? <ArrowRight className='size-4 text-[var(--text-icon)]' /> : undefined
129-
}
127+
trailing={<ArrowRight className='size-4 text-[var(--text-icon)]' />}
130128
/>
131129
</button>
132130
))}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,6 @@ export function useWand({
136136
setError(null)
137137
}, [])
138138

139-
const openPrompt = useCallback(() => {
140-
setIsPromptVisible(true)
141-
setPromptInputValue('')
142-
}, [])
143-
144-
const closePrompt = useCallback(() => {
145-
if (isLoading) return
146-
setIsPromptVisible(false)
147-
setPromptInputValue('')
148-
}, [isLoading])
149-
150139
const generateStream = useCallback(
151140
async ({ prompt }: { prompt: string }) => {
152141
if (!prompt) {
@@ -294,8 +283,6 @@ export function useWand({
294283
generateStream,
295284
showPromptInline,
296285
hidePromptInline,
297-
openPrompt,
298-
closePrompt,
299286
updatePromptValue,
300287
cancelGeneration,
301288
}

0 commit comments

Comments
 (0)