mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(mothership): inline rename for resource tabs + workspace_file rename tool
- Add double-click inline rename on file and table resource tabs - Wire useInlineRename + useRenameWorkspaceFile/useRenameTable mutations - Add rename operation to workspace_file copilot tool (schema, server, router) - Add knowledge base resource support (type, extraction, rendering, actions) - Accept optional className on InlineRenameInput for context-specific sizing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
interface InlineRenameInputProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
onCancel: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function InlineRenameInput({ value, onChange, onSubmit, onCancel }: InlineRenameInputProps) {
|
||||
export function InlineRenameInput({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
className,
|
||||
}: InlineRenameInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +40,10 @@ export function InlineRenameInput({ value, onChange, onSubmit, onCancel }: Inlin
|
||||
}}
|
||||
onBlur={onSubmit}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className='min-w-0 flex-1 truncate border-0 bg-transparent p-0 font-medium text-[14px] text-[var(--text-body)] outline-none focus:outline-none focus:ring-0'
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate border-0 bg-transparent p-0 font-medium text-[14px] text-[var(--text-body)] outline-none focus:outline-none focus:ring-0',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'
|
||||
export {
|
||||
EmbeddedKnowledgeBaseActions,
|
||||
EmbeddedWorkflowActions,
|
||||
ResourceContent,
|
||||
} from './resource-content'
|
||||
export { ResourceTabs } from './resource-tabs'
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'
|
||||
export {
|
||||
EmbeddedKnowledgeBaseActions,
|
||||
EmbeddedWorkflowActions,
|
||||
ResourceContent,
|
||||
} from './resource-content'
|
||||
|
||||
@@ -4,12 +4,14 @@ import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
|
||||
import { Square } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
|
||||
import { BookOpen } from '@/components/emcn/icons'
|
||||
import { WorkflowIcon } from '@/components/icons'
|
||||
import {
|
||||
FileViewer,
|
||||
type PreviewMode,
|
||||
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { KnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/[id]/base'
|
||||
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
|
||||
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
|
||||
@@ -62,6 +64,16 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
case 'knowledgebase':
|
||||
return (
|
||||
<KnowledgeBase
|
||||
key={resource.id}
|
||||
id={resource.id}
|
||||
knowledgeBaseName={resource.title}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -147,6 +159,40 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor
|
||||
)
|
||||
}
|
||||
|
||||
interface EmbeddedKnowledgeBaseActionsProps {
|
||||
workspaceId: string
|
||||
knowledgeBaseId: string
|
||||
}
|
||||
|
||||
export function EmbeddedKnowledgeBaseActions({
|
||||
workspaceId,
|
||||
knowledgeBaseId,
|
||||
}: EmbeddedKnowledgeBaseActionsProps) {
|
||||
const router = useRouter()
|
||||
|
||||
const handleOpenKnowledgeBase = useCallback(() => {
|
||||
router.push(`/workspace/${workspaceId}/knowledge/${knowledgeBaseId}`)
|
||||
}, [router, workspaceId, knowledgeBaseId])
|
||||
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='subtle'
|
||||
onClick={handleOpenKnowledgeBase}
|
||||
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
|
||||
aria-label='Open knowledge base'
|
||||
>
|
||||
<BookOpen className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
<p>Open Knowledge Base</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
interface EmbeddedFileProps {
|
||||
workspaceId: string
|
||||
fileId: string
|
||||
|
||||
@@ -8,10 +8,11 @@ import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { PanelLeft, Table as TableIcon } from '@/components/emcn/icons'
|
||||
import { BookOpen, PanelLeft, Table as TableIcon } from '@/components/emcn/icons'
|
||||
import { WorkflowIcon } from '@/components/icons'
|
||||
import { getDocumentIcon } from '@/components/icons/document-icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { InlineRenameInput } from '@/app/workspace/[workspaceId]/components/inline-rename-input'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import type {
|
||||
MothershipResource,
|
||||
@@ -47,6 +48,8 @@ function PreviewModeIcon({ mode, ...props }: { mode: PreviewMode } & SVGProps<SV
|
||||
)
|
||||
}
|
||||
|
||||
const RENAMABLE_TYPES = new Set(['file', 'table'])
|
||||
|
||||
interface ResourceTabsProps {
|
||||
resources: MothershipResource[]
|
||||
activeId: string | null
|
||||
@@ -55,11 +58,18 @@ interface ResourceTabsProps {
|
||||
previewMode?: PreviewMode
|
||||
onCyclePreviewMode?: () => void
|
||||
actions?: ReactNode
|
||||
editingId?: string | null
|
||||
editValue?: string
|
||||
onEditValueChange?: (value: string) => void
|
||||
onStartRename?: (id: string, currentName: string) => void
|
||||
onSubmitRename?: () => void
|
||||
onCancelRename?: () => void
|
||||
}
|
||||
|
||||
const RESOURCE_ICONS: Record<Exclude<MothershipResourceType, 'file'>, ElementType> = {
|
||||
table: TableIcon,
|
||||
workflow: WorkflowIcon,
|
||||
knowledgebase: BookOpen,
|
||||
}
|
||||
|
||||
function getResourceIcon(resource: MothershipResource): ElementType {
|
||||
@@ -81,6 +91,12 @@ export function ResourceTabs({
|
||||
previewMode,
|
||||
onCyclePreviewMode,
|
||||
actions,
|
||||
editingId,
|
||||
editValue,
|
||||
onEditValueChange,
|
||||
onStartRename,
|
||||
onSubmitRename,
|
||||
onCancelRename,
|
||||
}: ResourceTabsProps) {
|
||||
const scrollRef = useCallback<RefCallback<HTMLDivElement>>((node) => {
|
||||
if (!node) return
|
||||
@@ -118,20 +134,37 @@ export function ResourceTabs({
|
||||
{resources.map((resource) => {
|
||||
const Icon = getResourceIcon(resource)
|
||||
const isActive = activeId === resource.id
|
||||
const isEditing = editingId === resource.id
|
||||
const isRenamable = RENAMABLE_TYPES.has(resource.type)
|
||||
|
||||
return (
|
||||
<Tooltip.Root key={resource.id}>
|
||||
<Tooltip.Root key={resource.id} open={isEditing ? false : undefined}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='subtle'
|
||||
onClick={() => onSelect(resource.id)}
|
||||
onDoubleClick={
|
||||
isRenamable && onStartRename
|
||||
? () => onStartRename(resource.id, resource.title)
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'shrink-0 bg-transparent px-[8px] py-[4px] text-[12px]',
|
||||
isActive && 'bg-[var(--surface-4)]'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn('mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]')} />
|
||||
{resource.title}
|
||||
{isEditing && onEditValueChange && onSubmitRename && onCancelRename ? (
|
||||
<InlineRenameInput
|
||||
value={editValue ?? ''}
|
||||
onChange={onEditValueChange}
|
||||
onSubmit={onSubmitRename}
|
||||
onCancel={onCancelRename}
|
||||
className='min-w-[60px] max-w-[160px] text-[12px]'
|
||||
/>
|
||||
) : (
|
||||
resource.title
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { EmbeddedWorkflowActions, ResourceContent, ResourceTabs } from './components'
|
||||
import { useRenameTable } from '@/hooks/queries/tables'
|
||||
import { useRenameWorkspaceFile } from '@/hooks/queries/workspace-files'
|
||||
import { useInlineRename } from '@/hooks/use-inline-rename'
|
||||
import {
|
||||
EmbeddedKnowledgeBaseActions,
|
||||
EmbeddedWorkflowActions,
|
||||
ResourceContent,
|
||||
ResourceTabs,
|
||||
} from './components'
|
||||
|
||||
const PREVIEWABLE_EXTENSIONS = new Set(['md', 'html', 'htm', 'csv'])
|
||||
const PREVIEW_ONLY_EXTENSIONS = new Set(['html', 'htm'])
|
||||
@@ -21,13 +29,14 @@ interface MothershipViewProps {
|
||||
resources: MothershipResource[]
|
||||
activeResourceId: string | null
|
||||
onSelectResource: (id: string) => void
|
||||
onRenameResource: (id: string, newTitle: string) => void
|
||||
onCollapse: () => void
|
||||
isCollapsed: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Split-pane view that renders embedded resources (tables, files, workflows)
|
||||
* Split-pane view that renders embedded resources (tables, files, workflows, knowledge bases)
|
||||
* alongside the chat conversation. Composes ResourceTabs for navigation
|
||||
* and ResourceContent for rendering the active resource.
|
||||
*/
|
||||
@@ -36,12 +45,40 @@ export function MothershipView({
|
||||
resources,
|
||||
activeResourceId,
|
||||
onSelectResource,
|
||||
onRenameResource,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
className,
|
||||
}: MothershipViewProps) {
|
||||
const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null
|
||||
|
||||
const { mutate: renameFile } = useRenameWorkspaceFile()
|
||||
const { mutate: renameTable } = useRenameTable(workspaceId)
|
||||
|
||||
const resourcesRef = useRef(resources)
|
||||
useEffect(() => {
|
||||
resourcesRef.current = resources
|
||||
}, [resources])
|
||||
|
||||
const handleRenameSave = useCallback(
|
||||
(id: string, newName: string) => {
|
||||
const resource = resourcesRef.current.find((r) => r.id === id)
|
||||
if (!resource) return
|
||||
|
||||
onRenameResource(id, newName)
|
||||
|
||||
if (resource.type === 'file') {
|
||||
renameFile({ workspaceId, fileId: id, name: newName })
|
||||
} else if (resource.type === 'table') {
|
||||
renameTable({ tableId: id, name: newName })
|
||||
}
|
||||
},
|
||||
[onRenameResource, renameFile, renameTable, workspaceId]
|
||||
)
|
||||
|
||||
const { editingId, editValue, setEditValue, startRename, submitRename, cancelRename } =
|
||||
useInlineRename({ onSave: handleRenameSave })
|
||||
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>('split')
|
||||
const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), [])
|
||||
|
||||
@@ -56,6 +93,8 @@ export function MothershipView({
|
||||
const headerActions =
|
||||
active?.type === 'workflow' ? (
|
||||
<EmbeddedWorkflowActions workspaceId={workspaceId} workflowId={active.id} />
|
||||
) : active?.type === 'knowledgebase' ? (
|
||||
<EmbeddedKnowledgeBaseActions workspaceId={workspaceId} knowledgeBaseId={active.id} />
|
||||
) : null
|
||||
|
||||
return (
|
||||
@@ -75,6 +114,12 @@ export function MothershipView({
|
||||
actions={headerActions}
|
||||
previewMode={isActivePreviewable ? previewMode : undefined}
|
||||
onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined}
|
||||
editingId={editingId}
|
||||
editValue={editValue}
|
||||
onEditValueChange={setEditValue}
|
||||
onStartRename={startRename}
|
||||
onSubmitRename={submitRename}
|
||||
onCancelRename={cancelRename}
|
||||
/>
|
||||
<div className='min-h-0 flex-1 overflow-hidden'>
|
||||
{active && (
|
||||
|
||||
@@ -169,6 +169,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
resources,
|
||||
activeResourceId,
|
||||
setActiveResourceId,
|
||||
renameResource,
|
||||
} = useChat(workspaceId, chatId)
|
||||
|
||||
const [isResourceCollapsed, setIsResourceCollapsed] = useState(false)
|
||||
@@ -346,6 +347,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
resources={resources}
|
||||
activeResourceId={activeResourceId}
|
||||
onSelectResource={setActiveResourceId}
|
||||
onRenameResource={renameResource}
|
||||
onCollapse={collapseResource}
|
||||
isCollapsed={isResourceCollapsed}
|
||||
className={animateResourcePanel ? 'animate-slide-in-right' : undefined}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
|
||||
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
|
||||
import { tableKeys } from '@/hooks/queries/tables'
|
||||
import {
|
||||
type TaskChatHistory,
|
||||
@@ -29,9 +30,11 @@ import type {
|
||||
import {
|
||||
extractFileResource,
|
||||
extractFunctionExecuteResource,
|
||||
extractKnowledgeBaseResource,
|
||||
extractResourcesFromHistory,
|
||||
extractTableResource,
|
||||
extractWorkflowResource,
|
||||
GENERIC_TITLES,
|
||||
RESOURCE_TOOL_NAMES,
|
||||
} from '../utils'
|
||||
|
||||
@@ -44,6 +47,7 @@ export interface UseChatReturn {
|
||||
resources: MothershipResource[]
|
||||
activeResourceId: string | null
|
||||
setActiveResourceId: (id: string | null) => void
|
||||
renameResource: (id: string, newTitle: string) => void
|
||||
}
|
||||
|
||||
const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
|
||||
@@ -160,11 +164,15 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
|
||||
const { data: chatHistory } = useChatHistory(initialChatId)
|
||||
|
||||
const renameResource = useCallback((id: string, newTitle: string) => {
|
||||
setResources((prev) => prev.map((r) => (r.id === id ? { ...r, title: newTitle } : r)))
|
||||
}, [])
|
||||
|
||||
const addResource = useCallback((resource: MothershipResource) => {
|
||||
setResources((prev) => {
|
||||
const existing = prev.find((r) => r.type === resource.type && r.id === resource.id)
|
||||
if (existing) {
|
||||
const keepOldTitle = existing.title !== 'Table' && existing.title !== 'File'
|
||||
const keepOldTitle = !GENERIC_TITLES.has(existing.title)
|
||||
const title = keepOldTitle ? existing.title : resource.title
|
||||
if (title === existing.title) return prev
|
||||
return prev.map((r) =>
|
||||
@@ -468,6 +476,16 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
registry.loadWorkflowState(resource.id)
|
||||
}
|
||||
}
|
||||
} else if (toolName === 'knowledge') {
|
||||
resource = extractKnowledgeBaseResource(parsed, storedArgs)
|
||||
if (resource) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.detail(resource.id),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: knowledgeKeys.list(workspaceId),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (resource) addResource(resource)
|
||||
@@ -723,5 +741,6 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
resources,
|
||||
activeResourceId,
|
||||
setActiveResourceId,
|
||||
renameResource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ export interface SSEPayload {
|
||||
subagent?: string
|
||||
}
|
||||
|
||||
export type MothershipResourceType = 'table' | 'file' | 'workflow'
|
||||
export type MothershipResourceType = 'table' | 'file' | 'workflow' | 'knowledgebase'
|
||||
|
||||
export interface MothershipResource {
|
||||
type: MothershipResourceType
|
||||
|
||||
@@ -8,13 +8,21 @@ export const RESOURCE_TOOL_NAMES = new Set([
|
||||
'edit_workflow',
|
||||
'function_execute',
|
||||
'read',
|
||||
'knowledge',
|
||||
])
|
||||
|
||||
/**
|
||||
* Resolves the top-level result object from an SSE payload.
|
||||
* The result may arrive at `parsed.result` or nested under `parsed.data.result`.
|
||||
*/
|
||||
function getTopResult(parsed: SSEPayload): Record<string, unknown> | undefined {
|
||||
return (parsed.result ?? (typeof parsed.data === 'object' ? parsed.data?.result : undefined)) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
}
|
||||
|
||||
function getResultData(parsed: SSEPayload): Record<string, unknown> | undefined {
|
||||
const topResult = parsed.result as Record<string, unknown> | undefined
|
||||
const nestedResult =
|
||||
typeof parsed.data === 'object' ? (parsed.data?.result as Record<string, unknown>) : undefined
|
||||
const result = topResult ?? nestedResult
|
||||
const result = getTopResult(parsed)
|
||||
return result?.data as Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
@@ -66,10 +74,7 @@ export function extractFunctionExecuteResource(
|
||||
parsed: SSEPayload,
|
||||
storedArgs: Record<string, unknown> | undefined
|
||||
): MothershipResource | null {
|
||||
const topResult = (parsed.result ??
|
||||
(typeof parsed.data === 'object' ? parsed.data?.result : undefined)) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
const topResult = getTopResult(parsed)
|
||||
|
||||
if (topResult?.tableId) {
|
||||
return {
|
||||
@@ -94,10 +99,7 @@ export function extractWorkflowResource(
|
||||
parsed: SSEPayload,
|
||||
fallbackWorkflowId: string | null
|
||||
): MothershipResource | null {
|
||||
const topResult = (parsed.result ??
|
||||
(typeof parsed.data === 'object' ? parsed.data?.result : undefined)) as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
const topResult = getTopResult(parsed)
|
||||
const data = topResult?.data as Record<string, unknown> | undefined
|
||||
|
||||
const workflowId =
|
||||
@@ -110,7 +112,29 @@ export function extractWorkflowResource(
|
||||
return null
|
||||
}
|
||||
|
||||
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow'])
|
||||
export function extractKnowledgeBaseResource(
|
||||
parsed: SSEPayload,
|
||||
storedArgs: Record<string, unknown> | undefined
|
||||
): MothershipResource | null {
|
||||
const topResult = getTopResult(parsed)
|
||||
const data = topResult?.data as Record<string, unknown> | undefined
|
||||
|
||||
const knowledgeBaseId =
|
||||
(data?.id as string) ??
|
||||
(topResult?.knowledgeBaseId as string) ??
|
||||
(data?.knowledgeBaseId as string) ??
|
||||
(storedArgs?.knowledgeBaseId as string)
|
||||
const knowledgeBaseName =
|
||||
(data?.name as string) ?? (topResult?.knowledgeBaseName as string) ?? 'Knowledge Base'
|
||||
|
||||
if (knowledgeBaseId) {
|
||||
return { type: 'knowledgebase', id: knowledgeBaseId, title: knowledgeBaseName }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base'])
|
||||
|
||||
/**
|
||||
* Reconstructs the MothershipResource list from persisted tool calls.
|
||||
@@ -157,6 +181,8 @@ export function extractResourcesFromHistory(messages: TaskStoredMessage[]): Moth
|
||||
} else if (tc.name === 'create_workflow' || tc.name === 'edit_workflow') {
|
||||
resource = extractWorkflowResource(payload, lastWorkflowId)
|
||||
if (resource) lastWorkflowId = resource.id
|
||||
} else if (tc.name === 'knowledge') {
|
||||
resource = extractKnowledgeBaseResource(payload, args)
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
|
||||
@@ -91,6 +91,7 @@ const DOCUMENT_COLUMNS: ResourceColumn[] = [
|
||||
interface KnowledgeBaseProps {
|
||||
id: string
|
||||
knowledgeBaseName?: string
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
const AnimatedLoader = ({ className }: { className?: string }) => (
|
||||
@@ -185,9 +186,10 @@ function getDocumentTags(doc: DocumentData, definitions: TagDefinition[]): TagVa
|
||||
export function KnowledgeBase({
|
||||
id,
|
||||
knowledgeBaseName: passedKnowledgeBaseName,
|
||||
workspaceId: propWorkspaceId,
|
||||
}: KnowledgeBaseProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const workspaceId = propWorkspaceId || (params.workspaceId as string)
|
||||
const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false })
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools
|
||||
import {
|
||||
deleteWorkspaceFile,
|
||||
getWorkspaceFile,
|
||||
renameWorkspaceFile,
|
||||
updateWorkspaceFileContent,
|
||||
uploadWorkspaceFile,
|
||||
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
|
||||
@@ -125,6 +126,39 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
}
|
||||
}
|
||||
|
||||
case 'rename': {
|
||||
const fileId = (args as Record<string, unknown>).fileId as string | undefined
|
||||
const newName = (args as Record<string, unknown>).newName as string | undefined
|
||||
|
||||
if (!fileId) {
|
||||
return { success: false, message: 'fileId is required for rename operation' }
|
||||
}
|
||||
if (!newName) {
|
||||
return { success: false, message: 'newName is required for rename operation' }
|
||||
}
|
||||
|
||||
const fileRecord = await getWorkspaceFile(workspaceId, fileId)
|
||||
if (!fileRecord) {
|
||||
return { success: false, message: `File with ID "${fileId}" not found` }
|
||||
}
|
||||
|
||||
const oldName = fileRecord.name
|
||||
await renameWorkspaceFile(workspaceId, fileId, newName)
|
||||
|
||||
logger.info('Workspace file renamed via copilot', {
|
||||
fileId,
|
||||
oldName,
|
||||
newName,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File renamed from "${oldName}" to "${newName}"`,
|
||||
data: { id: fileId, name: newName },
|
||||
}
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const fileId = (args as Record<string, unknown>).fileId as string | undefined
|
||||
if (!fileId) {
|
||||
@@ -154,7 +188,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
message: `Unknown operation: ${operation}. Supported: write, delete. Use the filesystem to list/read files.`,
|
||||
message: `Unknown operation: ${operation}. Supported: write, update, rename, delete. Use the filesystem to list/read files.`,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -57,7 +57,7 @@ const WRITE_ACTIONS: Record<string, string[]> = {
|
||||
manage_mcp_tool: ['add', 'edit', 'delete'],
|
||||
manage_skill: ['add', 'edit', 'delete'],
|
||||
manage_credential: ['rename', 'delete'],
|
||||
workspace_file: ['write', 'update', 'delete'],
|
||||
workspace_file: ['write', 'update', 'delete', 'rename'],
|
||||
}
|
||||
|
||||
function isActionAllowed(toolName: string, action: string, userPermission: string): boolean {
|
||||
|
||||
@@ -172,7 +172,7 @@ export type UserTableResult = z.infer<typeof UserTableResultSchema>
|
||||
|
||||
// workspace_file - shared schema used by server tool and Go catalog
|
||||
export const WorkspaceFileArgsSchema = z.object({
|
||||
operation: z.enum(['write', 'update', 'delete']),
|
||||
operation: z.enum(['write', 'update', 'delete', 'rename']),
|
||||
args: z
|
||||
.object({
|
||||
fileId: z.string().optional(),
|
||||
@@ -180,6 +180,8 @@ export const WorkspaceFileArgsSchema = z.object({
|
||||
content: z.string().optional(),
|
||||
contentType: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
/** New name for the file (required for rename operation) */
|
||||
newName: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user