mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement: notifications, terminal, globals
This commit is contained in:
@@ -667,16 +667,36 @@ input[type="search"]::-ms-clear {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification toast enter animation
|
||||
* Notification toast enter animation — pop-open with stack offset
|
||||
*/
|
||||
@keyframes notification-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-16px);
|
||||
transform: translateX(calc(var(--stack-offset, 0px) - 8px)) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(var(--stack-offset, 0px));
|
||||
transform: translateX(var(--stack-offset, 0px)) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes notification-countdown {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 34.56;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes notification-exit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(var(--stack-offset, 0px)) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(calc(var(--stack-offset, 0px) + 8px)) scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import type { ChatResource, ResourceType } from '@/lib/copilot/resources'
|
||||
import {
|
||||
authenticateCopilotRequestSessionOnly,
|
||||
createBadRequestResponse,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
createNotFoundResponse,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import type { ChatResource, ResourceType } from '@/lib/copilot/resources'
|
||||
|
||||
const logger = createLogger('CopilotChatResourcesAPI')
|
||||
|
||||
@@ -75,7 +75,9 @@ export async function POST(req: NextRequest) {
|
||||
let merged: ChatResource[]
|
||||
if (prev) {
|
||||
if (GENERIC_TITLES.has(prev.title) && !GENERIC_TITLES.has(resource.title)) {
|
||||
merged = existing.map((r) => (`${r.type}:${r.id}` === key ? { ...r, title: resource.title } : r))
|
||||
merged = existing.map((r) =>
|
||||
`${r.type}:${r.id}` === key ? { ...r, title: resource.title } : r
|
||||
)
|
||||
} else {
|
||||
merged = existing
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type RefCallback,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { type RefCallback, useCallback, useMemo, useState } from 'react'
|
||||
import { ChevronRight, Folder } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
@@ -19,20 +15,19 @@ import {
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Plus, Search } from '@/components/emcn/icons'
|
||||
import { ChevronRight, Folder } from 'lucide-react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
|
||||
import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { useFolders } from '@/hooks/queries/folders'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import type { FolderTreeNode } from '@/stores/folders/types'
|
||||
import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
|
||||
|
||||
export interface AddResourceDropdownProps {
|
||||
workspaceId: string
|
||||
@@ -54,34 +49,54 @@ const EMPTY_SUBMENU = (
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
||||
export function useAvailableResources(workspaceId: string, existingKeys: Set<string>): AvailableItemsByType[] {
|
||||
export function useAvailableResources(
|
||||
workspaceId: string,
|
||||
existingKeys: Set<string>
|
||||
): AvailableItemsByType[] {
|
||||
const { data: workflows = [] } = useWorkflows(workspaceId, { syncRegistry: false })
|
||||
const { data: tables = [] } = useTablesList(workspaceId)
|
||||
const { data: files = [] } = useWorkspaceFiles(workspaceId)
|
||||
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
|
||||
|
||||
return useMemo(() => [
|
||||
{
|
||||
type: 'workflow' as const,
|
||||
items: workflows
|
||||
.map((w) => ({ id: w.id, name: w.name, color: w.color, folderId: w.folderId, isOpen: existingKeys.has(`workflow:${w.id}`) })),
|
||||
},
|
||||
{
|
||||
type: 'table' as const,
|
||||
items: tables
|
||||
.map((t) => ({ id: t.id, name: t.name, isOpen: existingKeys.has(`table:${t.id}`) })),
|
||||
},
|
||||
{
|
||||
type: 'file' as const,
|
||||
items: files
|
||||
.map((f) => ({ id: f.id, name: f.name, isOpen: existingKeys.has(`file:${f.id}`) })),
|
||||
},
|
||||
{
|
||||
type: 'knowledgebase' as const,
|
||||
items: (knowledgeBases ?? [])
|
||||
.map((kb) => ({ id: kb.id, name: kb.name, isOpen: existingKeys.has(`knowledgebase:${kb.id}`) })),
|
||||
},
|
||||
], [workflows, tables, files, knowledgeBases, existingKeys])
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
type: 'workflow' as const,
|
||||
items: workflows.map((w) => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
color: w.color,
|
||||
folderId: w.folderId,
|
||||
isOpen: existingKeys.has(`workflow:${w.id}`),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'table' as const,
|
||||
items: tables.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
isOpen: existingKeys.has(`table:${t.id}`),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'file' as const,
|
||||
items: files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
isOpen: existingKeys.has(`file:${f.id}`),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'knowledgebase' as const,
|
||||
items: (knowledgeBases ?? []).map((kb) => ({
|
||||
id: kb.id,
|
||||
name: kb.name,
|
||||
isOpen: existingKeys.has(`knowledgebase:${kb.id}`),
|
||||
})),
|
||||
},
|
||||
],
|
||||
[workflows, tables, files, knowledgeBases, existingKeys]
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleFolder({
|
||||
@@ -101,9 +116,7 @@ function CollapsibleFolder({
|
||||
config: ReturnType<typeof getResourceConfig>
|
||||
level: number
|
||||
}) {
|
||||
const folderWorkflows = workflows.filter(
|
||||
(w) => (w.folderId as string | null) === folder.id
|
||||
)
|
||||
const folderWorkflows = workflows.filter((w) => (w.folderId as string | null) === folder.id)
|
||||
const isExpanded = expanded.has(folder.id)
|
||||
const indent = level * 12
|
||||
|
||||
@@ -112,8 +125,13 @@ function CollapsibleFolder({
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onClick={(e) => { e.preventDefault(); onToggle(folder.id) }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') onToggle(folder.id) }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onToggle(folder.id)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onToggle(folder.id)
|
||||
}}
|
||||
className='flex cursor-pointer items-center gap-[6px] rounded-sm px-[8px] py-[6px] text-[13px] hover:bg-[var(--surface-active)]'
|
||||
style={{ paddingLeft: `${8 + indent}px` }}
|
||||
>
|
||||
@@ -169,7 +187,10 @@ function WorkflowSubmenuContent({
|
||||
useFolders(workspaceId)
|
||||
const folders = useFolderStore((state) => state.folders)
|
||||
const getFolderTree = useFolderStore((state) => state.getFolderTree)
|
||||
const folderTree = useMemo(() => getFolderTree(workspaceId), [folders, getFolderTree, workspaceId])
|
||||
const folderTree = useMemo(
|
||||
() => getFolderTree(workspaceId),
|
||||
[folders, getFolderTree, workspaceId]
|
||||
)
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleFolder = useCallback((id: string) => {
|
||||
@@ -231,7 +252,12 @@ function WorkflowSubmenuContent({
|
||||
)
|
||||
}
|
||||
|
||||
export function AddResourceDropdown({ workspaceId, existingKeys, onAdd, onSwitch }: AddResourceDropdownProps) {
|
||||
export function AddResourceDropdown({
|
||||
workspaceId,
|
||||
existingKeys,
|
||||
onAdd,
|
||||
onSwitch,
|
||||
}: AddResourceDropdownProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const available = useAvailableResources(workspaceId, existingKeys)
|
||||
@@ -338,7 +364,9 @@ export function AddResourceDropdown({ workspaceId, existingKeys, onAdd, onSwitch
|
||||
workspaceId={workspaceId}
|
||||
items={items}
|
||||
config={config}
|
||||
onSelect={(item) => select({ type, id: item.id, title: item.name }, item.isOpen)}
|
||||
onSelect={(item) =>
|
||||
select({ type, id: item.id, title: item.name }, item.isOpen)
|
||||
}
|
||||
/>
|
||||
) : items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { AddResourceDropdown } from './add-resource-dropdown'
|
||||
export type { AddResourceDropdownProps, AvailableItem } from './add-resource-dropdown'
|
||||
export { useAvailableResources } from './add-resource-dropdown'
|
||||
export { AddResourceDropdown, useAvailableResources } from './add-resource-dropdown'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AddResourceDropdown } from './add-resource-dropdown'
|
||||
export type { AddResourceDropdownProps, AvailableItem } from './add-resource-dropdown'
|
||||
export { useAvailableResources } from './add-resource-dropdown'
|
||||
export { AddResourceDropdown, useAvailableResources } from './add-resource-dropdown'
|
||||
export { ResourceActions, ResourceContent } from './resource-content'
|
||||
export { ResourceTabs } from './resource-tabs'
|
||||
|
||||
@@ -92,7 +92,9 @@ export function ResourceActions({ workspaceId, resource }: ResourceActionsProps)
|
||||
case 'workflow':
|
||||
return <EmbeddedWorkflowActions workspaceId={workspaceId} workflowId={resource.id} />
|
||||
case 'knowledgebase':
|
||||
return <EmbeddedKnowledgeBaseActions workspaceId={workspaceId} knowledgeBaseId={resource.id} />
|
||||
return (
|
||||
<EmbeddedKnowledgeBaseActions workspaceId={workspaceId} knowledgeBaseId={resource.id} />
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { type ElementType, type ReactNode } from 'react'
|
||||
import type { ElementType, ReactNode } from 'react'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { Database, File as FileIcon, Table as TableIcon } from '@/components/emcn/icons'
|
||||
import { WorkflowIcon } from '@/components/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getDocumentIcon } from '@/components/icons/document-icons'
|
||||
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
|
||||
import { tableKeys } from '@/hooks/queries/tables'
|
||||
import { workflowKeys } from '@/hooks/queries/workflows'
|
||||
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
|
||||
import { tableKeys } from '@/hooks/queries/tables'
|
||||
import { workflowKeys } from '@/hooks/queries/workflows'
|
||||
import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface DropdownItemRenderProps {
|
||||
@@ -87,7 +87,9 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
|
||||
type: 'table',
|
||||
label: 'Tables',
|
||||
icon: TableIcon,
|
||||
renderTabIcon: (_resource, className) => <TableIcon className={cn(className, 'text-[var(--text-icon)]')} />,
|
||||
renderTabIcon: (_resource, className) => (
|
||||
<TableIcon className={cn(className, 'text-[var(--text-icon)]')} />
|
||||
),
|
||||
renderDropdownItem: (props) => <DefaultDropdownItem {...props} />,
|
||||
},
|
||||
file: {
|
||||
@@ -104,7 +106,9 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
|
||||
type: 'knowledgebase',
|
||||
label: 'Knowledge Bases',
|
||||
icon: Database,
|
||||
renderTabIcon: (_resource, className) => <Database className={cn(className, 'text-[var(--text-icon)]')} />,
|
||||
renderTabIcon: (_resource, className) => (
|
||||
<Database className={cn(className, 'text-[var(--text-icon)]')} />
|
||||
),
|
||||
renderDropdownItem: (props) => <DefaultDropdownItem {...props} />,
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import {
|
||||
type ReactNode,
|
||||
type RefCallback,
|
||||
type SVGProps,
|
||||
type SVGProps,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { PanelLeft } from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import { useAddChatResource, useRemoveChatResource, useReorderChatResources } from '@/hooks/queries/tasks'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
|
||||
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
|
||||
import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
} from '@/app/workspace/[workspaceId]/home/types'
|
||||
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
|
||||
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
|
||||
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
|
||||
import { useTablesList } from '@/hooks/queries/tables'
|
||||
import {
|
||||
useAddChatResource,
|
||||
useRemoveChatResource,
|
||||
useReorderChatResources,
|
||||
} from '@/hooks/queries/tasks'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
|
||||
const LEFT_HALF =
|
||||
'M10.25 0.75H3.25C1.86929 0.75 0.75 1.86929 0.75 3.25V16.25C0.75 17.6307 1.86929 18.75 3.25 18.75H10.25V0.75Z'
|
||||
@@ -301,13 +301,21 @@ export function ResourceTabs({
|
||||
const isActive = activeId === resource.id
|
||||
const isHovered = hoveredTabId === resource.id
|
||||
const isDragging = draggedIdx === idx
|
||||
const showGapBefore = dropGapIdx === idx && draggedIdx !== null && draggedIdx !== idx && draggedIdx !== idx - 1
|
||||
const showGapAfter = idx === resources.length - 1 && dropGapIdx === resources.length && draggedIdx !== null && draggedIdx !== idx
|
||||
const showGapBefore =
|
||||
dropGapIdx === idx &&
|
||||
draggedIdx !== null &&
|
||||
draggedIdx !== idx &&
|
||||
draggedIdx !== idx - 1
|
||||
const showGapAfter =
|
||||
idx === resources.length - 1 &&
|
||||
dropGapIdx === resources.length &&
|
||||
draggedIdx !== null &&
|
||||
draggedIdx !== idx
|
||||
|
||||
return (
|
||||
<div key={resource.id} className='relative flex shrink-0 items-center'>
|
||||
{showGapBefore && (
|
||||
<div className='pointer-events-none absolute top-1/2 left-0 z-10 h-[16px] w-[2px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--text-subtle)]' />
|
||||
<div className='-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 left-0 z-10 h-[16px] w-[2px] rounded-full bg-[var(--text-subtle)]' />
|
||||
)}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
@@ -340,11 +348,22 @@ export function ResourceTabs({
|
||||
role='button'
|
||||
tabIndex={-1}
|
||||
onClick={(e) => handleRemove(e, resource)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleRemove(e as unknown as React.MouseEvent, resource) }}
|
||||
className='absolute right-[4px] top-1/2 flex -translate-y-1/2 items-center justify-center rounded-[4px] p-[1px] hover:bg-[var(--surface-5)]'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter')
|
||||
handleRemove(e as unknown as React.MouseEvent, resource)
|
||||
}}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[4px] flex items-center justify-center rounded-[4px] p-[1px] hover:bg-[var(--surface-5)]'
|
||||
aria-label={`Close ${displayName}`}
|
||||
>
|
||||
<svg className='h-[10px] w-[10px] text-[var(--text-tertiary)]' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='2.5' strokeLinecap='round' strokeLinejoin='round'>
|
||||
<svg
|
||||
className='h-[10px] w-[10px] text-[var(--text-tertiary)]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M18 6 6 18M6 6l12 12' />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -356,7 +375,7 @@ export function ResourceTabs({
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{showGapAfter && (
|
||||
<div className='pointer-events-none absolute top-1/2 right-0 z-10 h-[16px] w-[2px] translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--text-subtle)]' />
|
||||
<div className='-translate-y-1/2 pointer-events-none absolute top-1/2 right-0 z-10 h-[16px] w-[2px] translate-x-1/2 rounded-full bg-[var(--text-subtle)]' />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -398,4 +417,3 @@ export function ResourceTabs({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -91,9 +91,12 @@ export function MothershipView({
|
||||
/>
|
||||
) : (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-[4px] px-[24px]'>
|
||||
<h2 className='text-[20px] font-semibold text-[var(--text-secondary)]'>No resources open</h2>
|
||||
<h2 className='font-semibold text-[20px] text-[var(--text-secondary)]'>
|
||||
No resources open
|
||||
</h2>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Click the <span className='font-medium text-[var(--text-secondary)]'>+</span> button above to add a resource to this task
|
||||
Click the <span className='font-medium text-[var(--text-secondary)]'>+</span> button
|
||||
above to add a resource to this task
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -400,7 +400,13 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
onReorderResources={reorderResources}
|
||||
onCollapse={collapseResource}
|
||||
isCollapsed={isResourceCollapsed}
|
||||
className={isResourceAnimatingIn ? 'animate-slide-in-right' : skipResourceTransition ? '!transition-none' : undefined}
|
||||
className={
|
||||
isResourceAnimatingIn
|
||||
? 'animate-slide-in-right'
|
||||
: skipResourceTransition
|
||||
? '!transition-none'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{isResourceCollapsed && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
reportManualRunToolStop,
|
||||
} from '@/lib/copilot/client-sse/run-tool-execution'
|
||||
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
|
||||
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types'
|
||||
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
|
||||
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
||||
import {
|
||||
@@ -25,7 +26,6 @@ import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useTerminalConsoleStore } from '@/stores/terminal'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types'
|
||||
import { invalidateResourceQueries } from '../components/mothership-view/components/resource-registry'
|
||||
import type { FileAttachmentForApi } from '../components/user-input/user-input'
|
||||
import type {
|
||||
@@ -192,8 +192,7 @@ function extractResourceFromReadResult(
|
||||
const resourceType = VFS_DIR_TO_RESOURCE[segments[0]]
|
||||
if (!resourceType || !segments[1]) return null
|
||||
|
||||
const obj =
|
||||
output && typeof output === 'object' ? (output as Record<string, unknown>) : undefined
|
||||
const obj = output && typeof output === 'object' ? (output as Record<string, unknown>) : undefined
|
||||
if (!obj) return null
|
||||
|
||||
let id = obj.id as string | undefined
|
||||
@@ -256,12 +255,9 @@ export function useChat(
|
||||
setActiveResourceId(resource.id)
|
||||
}, [])
|
||||
|
||||
const removeResource = useCallback(
|
||||
(resourceType: MothershipResourceType, resourceId: string) => {
|
||||
setResources((prev) => prev.filter((r) => !(r.type === resourceType && r.id === resourceId)))
|
||||
},
|
||||
[]
|
||||
)
|
||||
const removeResource = useCallback((resourceType: MothershipResourceType, resourceId: string) => {
|
||||
setResources((prev) => prev.filter((r) => !(r.type === resourceType && r.id === resourceId)))
|
||||
}, [])
|
||||
|
||||
const reorderResources = useCallback((newOrder: MothershipResource[]) => {
|
||||
setResources(newOrder)
|
||||
@@ -524,12 +520,7 @@ export function useChat(
|
||||
)
|
||||
if (resource) {
|
||||
addResource(resource)
|
||||
invalidateResourceQueries(
|
||||
queryClient,
|
||||
workspaceId,
|
||||
resource.type,
|
||||
resource.id
|
||||
)
|
||||
invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id)
|
||||
onResourceEventRef.current?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { X } from 'lucide-react'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
@@ -6,6 +6,7 @@ import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/provide
|
||||
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import {
|
||||
type Notification,
|
||||
type NotificationAction,
|
||||
openCopilotWithMessage,
|
||||
useNotificationStore,
|
||||
@@ -14,11 +15,71 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('Notifications')
|
||||
const MAX_VISIBLE_NOTIFICATIONS = 4
|
||||
const STACK_OFFSET_PX = 3
|
||||
const AUTO_DISMISS_MS = 10000
|
||||
const EXIT_ANIMATION_MS = 200
|
||||
|
||||
const RING_RADIUS = 5.5
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
|
||||
|
||||
const ACTION_LABELS: Record<NotificationAction['type'], string> = {
|
||||
copilot: 'Fix in Copilot',
|
||||
refresh: 'Refresh',
|
||||
'unlock-workflow': 'Unlock Workflow',
|
||||
} as const
|
||||
|
||||
function isAutoDismissable(n: Notification): boolean {
|
||||
return n.level === 'error' && !!n.workflowId
|
||||
}
|
||||
|
||||
function CountdownRing({ onPause }: { onPause: () => void }) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={onPause}
|
||||
aria-label='Keep notifications visible'
|
||||
className='!p-[4px] -m-[2px] shrink-0 rounded-[5px] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<svg
|
||||
width='14'
|
||||
height='14'
|
||||
viewBox='0 0 16 16'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
style={{ transform: 'rotate(-90deg) scaleX(-1)' }}
|
||||
>
|
||||
<circle cx='8' cy='8' r={RING_RADIUS} stroke='var(--border)' strokeWidth='1.5' />
|
||||
<circle
|
||||
cx='8'
|
||||
cy='8'
|
||||
r={RING_RADIUS}
|
||||
stroke='var(--text-icon)'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeDasharray={RING_CIRCUMFERENCE}
|
||||
style={{
|
||||
animation: `notification-countdown ${AUTO_DISMISS_MS}ms linear forwards`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Keep visible</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifications display component.
|
||||
* Positioned in the bottom-right workspace area, reactive to panel width and terminal height.
|
||||
* Shows both global notifications and workflow-specific notifications.
|
||||
*
|
||||
* Workflow error notifications auto-dismiss after {@link AUTO_DISMISS_MS}ms with a countdown
|
||||
* ring. Clicking the ring pauses all timers until the notification stack clears.
|
||||
*/
|
||||
export const Notifications = memo(function Notifications() {
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
@@ -63,7 +124,6 @@ export const Notifications = memo(function Notifications() {
|
||||
logger.warn('Unknown action type', { notificationId, actionType: action.type })
|
||||
}
|
||||
|
||||
// Dismiss the notification after the action is triggered
|
||||
removeNotification(notificationId)
|
||||
} catch (error) {
|
||||
logger.error('Failed to execute notification action', {
|
||||
@@ -76,13 +136,6 @@ export const Notifications = memo(function Notifications() {
|
||||
[removeNotification]
|
||||
)
|
||||
|
||||
/**
|
||||
* Register global keyboard shortcut for clearing notifications.
|
||||
*
|
||||
* - Mod+E: Clear all notifications visible in the current workflow (including global ones).
|
||||
*
|
||||
* The command is disabled in editable contexts so it does not interfere with typing.
|
||||
*/
|
||||
useRegisterGlobalCommands(() =>
|
||||
createCommands([
|
||||
{
|
||||
@@ -99,19 +152,78 @@ export const Notifications = memo(function Notifications() {
|
||||
|
||||
const preventZoomRef = usePreventZoom()
|
||||
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
const [exitingIds, setExitingIds] = useState<Set<string>>(new Set())
|
||||
const timersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>())
|
||||
|
||||
const pauseAll = useCallback(() => {
|
||||
setIsPaused(true)
|
||||
setExitingIds(new Set())
|
||||
for (const timer of timersRef.current.values()) clearTimeout(timer)
|
||||
timersRef.current.clear()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Manages per-notification dismiss timers.
|
||||
* Resets pause state when the notification stack empties so new arrivals get fresh timers.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (visibleNotifications.length === 0) {
|
||||
if (isPaused) setIsPaused(false)
|
||||
return
|
||||
}
|
||||
if (isPaused) return
|
||||
|
||||
const timers = timersRef.current
|
||||
const activeIds = new Set<string>()
|
||||
|
||||
for (const n of visibleNotifications) {
|
||||
if (!isAutoDismissable(n) || timers.has(n.id)) continue
|
||||
activeIds.add(n.id)
|
||||
|
||||
timers.set(
|
||||
n.id,
|
||||
setTimeout(() => {
|
||||
timers.delete(n.id)
|
||||
setExitingIds((prev) => new Set(prev).add(n.id))
|
||||
setTimeout(() => {
|
||||
removeNotification(n.id)
|
||||
setExitingIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(n.id)
|
||||
return next
|
||||
})
|
||||
}, EXIT_ANIMATION_MS)
|
||||
}, AUTO_DISMISS_MS)
|
||||
)
|
||||
}
|
||||
|
||||
for (const [id, timer] of timers) {
|
||||
if (!activeIds.has(id) && !visibleNotifications.some((n) => n.id === id)) {
|
||||
clearTimeout(timer)
|
||||
timers.delete(id)
|
||||
}
|
||||
}
|
||||
}, [visibleNotifications, removeNotification, isPaused])
|
||||
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current
|
||||
return () => {
|
||||
for (const timer of timers.values()) clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (visibleNotifications.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={preventZoomRef}
|
||||
className='absolute right-[16px] bottom-[16px] z-30 flex flex-col items-start'
|
||||
>
|
||||
<div ref={preventZoomRef} className='absolute right-[16px] bottom-[16px] z-30 grid'>
|
||||
{[...visibleNotifications].reverse().map((notification, index, stacked) => {
|
||||
const depth = stacked.length - index - 1
|
||||
const xOffset = depth * 3
|
||||
const xOffset = depth * STACK_OFFSET_PX
|
||||
const hasAction = Boolean(notification.action)
|
||||
const showCountdown = !isPaused && isAutoDismissable(notification)
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -119,54 +231,48 @@ export const Notifications = memo(function Notifications() {
|
||||
style={
|
||||
{
|
||||
'--stack-offset': `${xOffset}px`,
|
||||
animation: 'notification-enter 200ms ease-out forwards',
|
||||
animation: exitingIds.has(notification.id)
|
||||
? `notification-exit ${EXIT_ANIMATION_MS}ms ease-in forwards`
|
||||
: 'notification-enter 200ms ease-out forwards',
|
||||
gridArea: '1 / 1',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={`relative h-[80px] w-[240px] overflow-hidden rounded-[4px] border bg-[var(--surface-2)] ${
|
||||
index > 0 ? '-mt-[80px]' : ''
|
||||
}`}
|
||||
className='w-[240px] self-end overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--bg)] shadow-sm'
|
||||
>
|
||||
<div className='flex h-full flex-col justify-between px-[8px] pt-[6px] pb-[8px]'>
|
||||
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||
<div className='flex items-start gap-[8px]'>
|
||||
<div
|
||||
className={`min-w-0 flex-1 font-medium text-[12px] leading-[16px] ${
|
||||
hasAction ? 'line-clamp-2' : 'line-clamp-4'
|
||||
}`}
|
||||
>
|
||||
<div className='line-clamp-2 min-w-0 flex-1 font-medium text-[12px] text-[var(--text-body)]'>
|
||||
{notification.level === 'error' && (
|
||||
<span className='mr-[6px] mb-[2.75px] inline-block h-[6px] w-[6px] rounded-[2px] bg-[var(--text-error)] align-middle' />
|
||||
<span className='mr-[8px] mb-[2px] inline-block h-[8px] w-[8px] rounded-[2px] bg-[var(--text-error)] align-middle' />
|
||||
)}
|
||||
{notification.message}
|
||||
</div>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeNotification(notification.id)}
|
||||
aria-label='Dismiss notification'
|
||||
className='!p-1.5 -m-1.5 shrink-0'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys='⌘E'>Clear all</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<div className='flex shrink-0 items-start gap-[2px]'>
|
||||
{showCountdown && <CountdownRing onPause={pauseAll} />}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeNotification(notification.id)}
|
||||
aria-label='Dismiss notification'
|
||||
className='!p-[4px] -m-[2px] shrink-0 rounded-[5px] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<X className='h-[14px] w-[14px] text-[var(--text-icon)]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys='⌘E'>Clear all</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</div>
|
||||
{hasAction && (
|
||||
<Button
|
||||
variant='active'
|
||||
onClick={() => executeAction(notification.id, notification.action!)}
|
||||
className='w-full px-[8px] py-[4px] font-medium text-[12px]'
|
||||
className='w-full rounded-[5px] px-[8px] py-[4px] font-medium text-[12px]'
|
||||
>
|
||||
{notification.action!.type === 'copilot'
|
||||
? 'Fix in Copilot'
|
||||
: notification.action!.type === 'refresh'
|
||||
? 'Refresh'
|
||||
: notification.action!.type === 'unlock-workflow'
|
||||
? 'Unlock Workflow'
|
||||
: 'Take action'}
|
||||
{ACTION_LABELS[notification.action!.type] ?? 'Take action'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ const SearchContext = createContext<SearchContextValue | null>(null)
|
||||
* Configuration for virtualized rendering.
|
||||
*/
|
||||
const CONFIG = {
|
||||
ROW_HEIGHT: 22,
|
||||
ROW_HEIGHT: 30,
|
||||
INDENT_PER_LEVEL: 12,
|
||||
BASE_PADDING: 20,
|
||||
MAX_SEARCH_DEPTH: 100,
|
||||
@@ -60,17 +60,16 @@ const BADGE_VARIANTS: Record<ValueType, BadgeVariant> = {
|
||||
* Styling constants matching the original non-virtualized implementation.
|
||||
*/
|
||||
const STYLES = {
|
||||
row: 'group flex min-h-[22px] cursor-pointer items-center gap-[6px] rounded-[8px] px-[6px] -mx-[6px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]',
|
||||
row: 'group flex min-h-[30px] cursor-pointer items-center gap-[8px] rounded-[8px] px-[8px] -mx-[8px] hover:bg-[var(--surface-active)]',
|
||||
chevron:
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
keyName:
|
||||
'font-medium text-[13px] text-[var(--text-primary)] group-hover:text-[var(--text-primary)]',
|
||||
'h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100',
|
||||
keyName: 'font-base text-[14px] text-[var(--text-primary)]',
|
||||
badge: 'rounded-[4px] px-[4px] py-[0px] text-[11px]',
|
||||
summary: 'text-[12px] text-[var(--text-tertiary)]',
|
||||
summary: 'text-[14px] text-[var(--text-secondary)]',
|
||||
indent:
|
||||
'mt-[2px] ml-[3px] flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[9px]',
|
||||
value: 'min-w-0 py-[2px] text-[13px] text-[var(--text-primary)]',
|
||||
emptyValue: 'py-[2px] text-[13px] text-[var(--text-tertiary)]',
|
||||
value: 'min-w-0 py-[2px] text-[14px] text-[var(--text-primary)]',
|
||||
emptyValue: 'py-[2px] text-[14px] text-[var(--text-secondary)]',
|
||||
matchHighlight: 'bg-yellow-200/60 dark:bg-yellow-500/40',
|
||||
currentMatchHighlight: 'bg-orange-400',
|
||||
} as const
|
||||
@@ -87,6 +86,7 @@ function getTypeLabel(value: unknown): ValueType {
|
||||
function formatPrimitive(value: unknown): string {
|
||||
if (value === null) return 'null'
|
||||
if (value === undefined) return 'undefined'
|
||||
if (typeof value === 'string') return JSON.stringify(value)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
|
||||
@@ -316,8 +316,8 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
<Button
|
||||
variant='ghost'
|
||||
className={clsx(
|
||||
'px-[8px] py-[6px] text-[12px]',
|
||||
!showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-tertiary)]'
|
||||
'px-[8px] py-[6px] text-small',
|
||||
!showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-icon)]'
|
||||
)}
|
||||
onClick={handleOutputButtonClick}
|
||||
aria-label='Show output'
|
||||
@@ -328,8 +328,8 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
<Button
|
||||
variant='ghost'
|
||||
className={clsx(
|
||||
'px-[8px] py-[6px] text-[12px]',
|
||||
showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-tertiary)]'
|
||||
'px-[8px] py-[6px] text-small',
|
||||
showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-icon)]'
|
||||
)}
|
||||
onClick={handleInputButtonClick}
|
||||
aria-label='Show input'
|
||||
@@ -361,7 +361,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Close search'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
<X className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
@@ -377,7 +377,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Search in output'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Search className='h-[12px] w-[12px]' />
|
||||
<Search className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
@@ -395,7 +395,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Component Playground'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Palette className='h-[12px] w-[12px]' />
|
||||
<Palette className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
@@ -418,9 +418,9 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
)}
|
||||
>
|
||||
{isTraining ? (
|
||||
<Pause className='h-[12px] w-[12px]' />
|
||||
<Pause className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<Database className='h-[12px] w-[12px]' />
|
||||
<Database className='h-3.5 w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
@@ -439,9 +439,9 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
{showCopySuccess ? (
|
||||
<Check className='h-[12px] w-[12px]' />
|
||||
<Check className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<Clipboard className='h-[12px] w-[12px]' />
|
||||
<Clipboard className='h-3.5 w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
@@ -459,7 +459,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Download console CSV'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<ArrowDownToLine className='h-3 w-3' />
|
||||
<ArrowDownToLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
@@ -474,7 +474,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Clear console'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Trash2 className='h-3 w-3' />
|
||||
<Trash2 className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
@@ -557,7 +557,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
className='!p-1.5 -m-1.5'
|
||||
disabled={matchCount === 0}
|
||||
>
|
||||
<ArrowUp className='h-[12px] w-[12px]' />
|
||||
<ArrowUp className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -566,7 +566,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
className='!p-1.5 -m-1.5'
|
||||
disabled={matchCount === 0}
|
||||
>
|
||||
<ArrowDown className='h-[12px] w-[12px]' />
|
||||
<ArrowDown className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -574,7 +574,7 @@ export const OutputPanel = React.memo(function OutputPanel({
|
||||
aria-label='Close search'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
<X className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -108,7 +108,7 @@ const BlockRow = memo(function BlockRow({
|
||||
data-entry-id={entry.id}
|
||||
className={clsx(
|
||||
ROW_STYLES.base,
|
||||
'h-[26px]',
|
||||
'h-[30px]',
|
||||
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -118,19 +118,15 @@ const BlockRow = memo(function BlockRow({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<div
|
||||
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||
{BlockIcon && <BlockIcon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 truncate font-medium text-[13px]',
|
||||
hasError
|
||||
? 'text-[var(--text-error)]'
|
||||
: isSelected
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
'min-w-0 truncate font-base text-[14px]',
|
||||
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{entry.blockName}
|
||||
@@ -138,9 +134,8 @@ const BlockRow = memo(function BlockRow({
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0 font-medium text-[13px]',
|
||||
!isRunning &&
|
||||
(isCanceled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]')
|
||||
'flex-shrink-0 font-base text-[14px]',
|
||||
!isRunning && 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
<StatusDisplay
|
||||
@@ -187,7 +182,7 @@ const IterationNodeRow = memo(function IterationNodeRow({
|
||||
<div className='flex min-w-0 flex-col'>
|
||||
{/* Iteration Header */}
|
||||
<div
|
||||
className={clsx(ROW_STYLES.base, 'h-[26px]', ROW_STYLES.hover)}
|
||||
className={clsx(ROW_STYLES.base, 'h-[30px]', ROW_STYLES.hover)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggle()
|
||||
@@ -196,10 +191,8 @@ const IterationNodeRow = memo(function IterationNodeRow({
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 truncate font-medium text-[13px]',
|
||||
hasError
|
||||
? 'text-[var(--text-error)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
'min-w-0 truncate font-base text-[14px]',
|
||||
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{iterationLabel}
|
||||
@@ -207,7 +200,7 @@ const IterationNodeRow = memo(function IterationNodeRow({
|
||||
{hasChildren && (
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
'h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100',
|
||||
!isExpanded && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
@@ -215,9 +208,8 @@ const IterationNodeRow = memo(function IterationNodeRow({
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0 font-medium text-[13px]',
|
||||
!hasRunningChild &&
|
||||
(hasCanceledChild ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]')
|
||||
'flex-shrink-0 font-base text-[14px]',
|
||||
!hasRunningChild && 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
<StatusDisplay
|
||||
@@ -285,7 +277,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
||||
<div className='flex min-w-0 flex-col'>
|
||||
{/* Subflow Header */}
|
||||
<div
|
||||
className={clsx(ROW_STYLES.base, 'h-[26px]', ROW_STYLES.hover)}
|
||||
className={clsx(ROW_STYLES.base, 'h-[30px]', ROW_STYLES.hover)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleNode(nodeId)
|
||||
@@ -293,19 +285,15 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<div
|
||||
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||
{BlockIcon && <BlockIcon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 truncate font-medium text-[13px]',
|
||||
hasError
|
||||
? 'text-[var(--text-error)]'
|
||||
: isExpanded
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
'min-w-0 truncate font-base text-[14px]',
|
||||
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{displayName}
|
||||
@@ -313,7 +301,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
||||
{hasChildren && (
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
'h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100',
|
||||
!isExpanded && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
@@ -321,11 +309,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0 font-medium text-[13px]',
|
||||
!hasRunningDescendant &&
|
||||
(hasCanceledDescendant
|
||||
? 'text-[var(--text-secondary)]'
|
||||
: 'text-[var(--text-tertiary)]')
|
||||
'flex-shrink-0 font-base text-[14px]',
|
||||
!hasRunningDescendant && 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
<StatusDisplay
|
||||
@@ -400,7 +385,7 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({
|
||||
<div
|
||||
className={clsx(
|
||||
ROW_STYLES.base,
|
||||
'h-[26px]',
|
||||
'h-[30px]',
|
||||
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -411,19 +396,15 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<div
|
||||
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||
{BlockIcon && <BlockIcon className='h-[10px] w-[10px] text-white' />}
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'min-w-0 truncate font-medium text-[13px]',
|
||||
hasError
|
||||
? 'text-[var(--text-error)]'
|
||||
: isSelected || isExpanded
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
'min-w-0 truncate font-base text-[14px]',
|
||||
hasError ? 'text-[var(--text-error)]' : 'text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{entry.blockName}
|
||||
@@ -431,7 +412,7 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({
|
||||
{hasChildren && (
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
'h-[7px] w-[9px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100',
|
||||
!isExpanded && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
@@ -439,11 +420,8 @@ const WorkflowNodeRow = memo(function WorkflowNodeRow({
|
||||
</div>
|
||||
<span
|
||||
className={clsx(
|
||||
'flex-shrink-0 font-medium text-[13px]',
|
||||
!hasRunningDescendant &&
|
||||
(hasCanceledDescendant
|
||||
? 'text-[var(--text-secondary)]'
|
||||
: 'text-[var(--text-tertiary)]')
|
||||
'flex-shrink-0 font-base text-[14px]',
|
||||
!hasRunningDescendant && 'text-[var(--text-secondary)]'
|
||||
)}
|
||||
>
|
||||
<StatusDisplay
|
||||
@@ -1341,9 +1319,9 @@ export const Terminal = memo(function Terminal() {
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
{sortConfig.direction === 'desc' ? (
|
||||
<ArrowDown className='h-3 w-3' />
|
||||
<ArrowDown className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<ArrowUp className='h-3 w-3' />
|
||||
<ArrowUp className='h-3.5 w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
@@ -1362,7 +1340,7 @@ export const Terminal = memo(function Terminal() {
|
||||
aria-label='Component Playground'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Palette className='h-3 w-3' />
|
||||
<Palette className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
@@ -1385,9 +1363,9 @@ export const Terminal = memo(function Terminal() {
|
||||
)}
|
||||
>
|
||||
{isTraining ? (
|
||||
<Pause className='h-3 w-3' />
|
||||
<Pause className='h-3.5 w-3.5' />
|
||||
) : (
|
||||
<Database className='h-3 w-3' />
|
||||
<Database className='h-3.5 w-3.5' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
@@ -1407,7 +1385,7 @@ export const Terminal = memo(function Terminal() {
|
||||
aria-label='Download console CSV'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<ArrowDownToLine className='h-3 w-3' />
|
||||
<ArrowDownToLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
@@ -1422,7 +1400,7 @@ export const Terminal = memo(function Terminal() {
|
||||
aria-label='Clear console'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Trash2 className='h-3 w-3' />
|
||||
<Trash2 className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
|
||||
@@ -50,9 +50,9 @@ export interface BlockInfo {
|
||||
* Common row styling classes for terminal components
|
||||
*/
|
||||
export const ROW_STYLES = {
|
||||
base: 'group flex cursor-pointer items-center justify-between gap-[8px] rounded-[8px] px-[6px]',
|
||||
selected: 'bg-[var(--surface-6)] dark:bg-[var(--surface-5)]',
|
||||
hover: 'hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]',
|
||||
base: 'group flex cursor-pointer items-center justify-between gap-[8px] rounded-[8px] px-[8px]',
|
||||
selected: 'bg-[var(--surface-active)]',
|
||||
hover: 'hover:bg-[var(--surface-active)]',
|
||||
nested:
|
||||
'mt-[2px] ml-[3px] flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[9px]',
|
||||
iconButton: '!p-1.5 -m-1.5',
|
||||
|
||||
@@ -694,5 +694,5 @@ export function flattenBlockEntriesOnly(
|
||||
export const TERMINAL_CONFIG = {
|
||||
NEAR_MIN_THRESHOLD: 40,
|
||||
BLOCK_COLUMN_WIDTH_PX: TERMINAL_BLOCK_COLUMN_WIDTH,
|
||||
HEADER_TEXT_CLASS: 'font-medium text-[var(--text-tertiary)] text-[12px]',
|
||||
HEADER_TEXT_CLASS: 'font-base text-[var(--text-icon)] text-small',
|
||||
} as const
|
||||
|
||||
@@ -355,11 +355,7 @@ export function WorkspaceHeader({
|
||||
<span className='min-w-0 flex-1 truncate text-left font-base text-[14px] text-[var(--text-primary)]'>
|
||||
{activeWorkspace?.name || 'Loading...'}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`sidebar-collapse-hide h-[8px] w-[10px] flex-shrink-0 text-[var(--text-muted)] transition-transform duration-100 group-hover:text-[var(--text-secondary)] ${
|
||||
isWorkspaceMenuOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
<ChevronDown className='sidebar-collapse-hide h-[8px] w-[10px] flex-shrink-0 text-[var(--text-muted)] group-hover:text-[var(--text-secondary)]' />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -72,22 +72,6 @@ import { useSidebarStore } from '@/stores/sidebar/store'
|
||||
|
||||
const logger = createLogger('Sidebar')
|
||||
|
||||
type TaskStatus = 'running' | 'unread' | 'idle'
|
||||
|
||||
function TaskStatusIcon({ status }: { status: TaskStatus }) {
|
||||
return (
|
||||
<div className='relative h-[16px] w-[16px] flex-shrink-0'>
|
||||
<Blimp className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
{status === 'running' && (
|
||||
<span className='-bottom-[1px] -right-[1px] absolute h-[7px] w-[7px] animate-pulse rounded-full border border-[var(--surface-1)] bg-[var(--brand-tertiary-2)]' />
|
||||
)}
|
||||
{status === 'unread' && (
|
||||
<span className='-bottom-[1px] -right-[1px] absolute h-[7px] w-[7px] rounded-full border border-[var(--surface-1)] bg-[var(--brand-tertiary)]' />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarItemSkeleton() {
|
||||
return (
|
||||
<div className='mx-[2px] flex h-[30px] items-center px-[8px]'>
|
||||
@@ -100,18 +84,22 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||
task,
|
||||
isCurrentRoute,
|
||||
isSelected,
|
||||
status,
|
||||
isActive,
|
||||
showCollapsedContent,
|
||||
onMultiSelectClick,
|
||||
onContextMenu,
|
||||
onMorePointerDown,
|
||||
onMoreClick,
|
||||
}: {
|
||||
task: { id: string; href: string; name: string }
|
||||
isCurrentRoute: boolean
|
||||
isSelected: boolean
|
||||
status: TaskStatus
|
||||
isActive: boolean
|
||||
showCollapsedContent: boolean
|
||||
onMultiSelectClick: (taskId: string, shiftKey: boolean, metaKey: boolean) => void
|
||||
onContextMenu: (e: React.MouseEvent, taskId: string) => void
|
||||
onMorePointerDown: () => void
|
||||
onMoreClick: (e: React.MouseEvent<HTMLButtonElement>, taskId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<Tooltip.Root>
|
||||
@@ -119,7 +107,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||
<Link
|
||||
href={task.href}
|
||||
className={cn(
|
||||
'mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)]',
|
||||
'group mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)]',
|
||||
(isCurrentRoute || isSelected) && 'bg-[var(--surface-active)]'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -136,8 +124,30 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||
}}
|
||||
onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined}
|
||||
>
|
||||
<TaskStatusIcon status={status} />
|
||||
<div className='min-w-0 truncate font-base text-[var(--text-body)]'>{task.name}</div>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<div className='min-w-0 flex-1 truncate font-base text-[var(--text-body)]'>
|
||||
{task.name}
|
||||
</div>
|
||||
{task.id !== 'new' && (
|
||||
<div className='relative flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center'>
|
||||
{isActive && (
|
||||
<span className='absolute h-[7px] w-[7px] rounded-full bg-[#33C482] group-hover:hidden' />
|
||||
)}
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Task options'
|
||||
onPointerDown={onMorePointerDown}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onMoreClick(e, task.id)
|
||||
}}
|
||||
className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px] opacity-0 hover:bg-[var(--surface-7)] group-hover:opacity-100'
|
||||
>
|
||||
<MoreHorizontal className='h-[16px] w-[16px] text-[var(--text-icon)]' />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
{showCollapsedContent && (
|
||||
@@ -390,6 +400,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
menuRef: taskMenuRef,
|
||||
handleContextMenu: handleTaskContextMenuBase,
|
||||
closeMenu: closeTaskContextMenu,
|
||||
preventDismiss: preventTaskDismiss,
|
||||
} = useContextMenu()
|
||||
|
||||
const contextMenuSelectionRef = useRef<{ taskIds: string[]; names: string[] }>({
|
||||
@@ -397,21 +408,49 @@ export const Sidebar = memo(function Sidebar() {
|
||||
names: [],
|
||||
})
|
||||
|
||||
const captureTaskSelection = useCallback((taskId: string) => {
|
||||
const { selectedTasks, selectTaskOnly } = useFolderStore.getState()
|
||||
if (selectedTasks.size > 0 && selectedTasks.has(taskId)) {
|
||||
contextMenuSelectionRef.current = {
|
||||
taskIds: Array.from(selectedTasks),
|
||||
names: [],
|
||||
}
|
||||
} else {
|
||||
selectTaskOnly(taskId)
|
||||
contextMenuSelectionRef.current = { taskIds: [taskId], names: [] }
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTaskContextMenu = useCallback(
|
||||
(e: React.MouseEvent, taskId: string) => {
|
||||
const { selectedTasks, selectTaskOnly } = useFolderStore.getState()
|
||||
if (selectedTasks.size > 0 && selectedTasks.has(taskId)) {
|
||||
contextMenuSelectionRef.current = {
|
||||
taskIds: Array.from(selectedTasks),
|
||||
names: [],
|
||||
}
|
||||
} else {
|
||||
selectTaskOnly(taskId)
|
||||
contextMenuSelectionRef.current = { taskIds: [taskId], names: [] }
|
||||
}
|
||||
captureTaskSelection(taskId)
|
||||
handleTaskContextMenuBase(e)
|
||||
},
|
||||
[handleTaskContextMenuBase]
|
||||
[captureTaskSelection, handleTaskContextMenuBase]
|
||||
)
|
||||
|
||||
const handleTaskMorePointerDown = useCallback(() => {
|
||||
if (isTaskContextMenuOpen) {
|
||||
preventTaskDismiss()
|
||||
}
|
||||
}, [isTaskContextMenuOpen, preventTaskDismiss])
|
||||
|
||||
const handleTaskMoreClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>, taskId: string) => {
|
||||
if (isTaskContextMenuOpen) {
|
||||
closeTaskContextMenu()
|
||||
return
|
||||
}
|
||||
captureTaskSelection(taskId)
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
handleTaskContextMenuBase({
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
clientX: rect.right,
|
||||
clientY: rect.top,
|
||||
} as React.MouseEvent)
|
||||
},
|
||||
[isTaskContextMenuOpen, closeTaskContextMenu, captureTaskSelection, handleTaskContextMenuBase]
|
||||
)
|
||||
|
||||
const { handleDuplicateWorkspace: duplicateWorkspace } = useDuplicateWorkspace({
|
||||
@@ -1048,13 +1087,6 @@ export const Sidebar = memo(function Sidebar() {
|
||||
const isCurrentRoute = task.id !== 'new' && pathname === task.href
|
||||
const isRenaming = renamingTaskId === task.id
|
||||
const isSelected = task.id !== 'new' && selectedTasks.has(task.id)
|
||||
const status: TaskStatus = isCurrentRoute
|
||||
? 'idle'
|
||||
: task.isActive
|
||||
? 'running'
|
||||
: task.isUnread
|
||||
? 'unread'
|
||||
: 'idle'
|
||||
|
||||
if (!isCollapsed && isRenaming) {
|
||||
return (
|
||||
@@ -1062,7 +1094,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
key={task.id}
|
||||
className='mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] bg-[var(--surface-active)] px-[8px] text-[14px]'
|
||||
>
|
||||
<TaskStatusIcon status={status} />
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
value={renameValue}
|
||||
@@ -1081,10 +1113,12 @@ export const Sidebar = memo(function Sidebar() {
|
||||
task={task}
|
||||
isCurrentRoute={isCurrentRoute}
|
||||
isSelected={isSelected}
|
||||
status={status}
|
||||
isActive={!!task.isActive}
|
||||
showCollapsedContent={showCollapsedContent}
|
||||
onMultiSelectClick={handleTaskClick}
|
||||
onContextMenu={handleTaskContextMenu}
|
||||
onMorePointerDown={handleTaskMorePointerDown}
|
||||
onMoreClick={handleTaskMoreClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -61,8 +61,8 @@ export { Search } from './search'
|
||||
export { Server } from './server'
|
||||
export { Settings } from './settings'
|
||||
export { ShieldCheck } from './shield-check'
|
||||
export { SquareArrowUpRight } from './square-arrow-up-right'
|
||||
export { Sim } from './sim'
|
||||
export { SquareArrowUpRight } from './square-arrow-up-right'
|
||||
export { Table } from './table'
|
||||
export { Tag } from './tag'
|
||||
export { TerminalWindow } from './terminal-window'
|
||||
|
||||
@@ -242,7 +242,9 @@ export function useAddChatResource(chatId?: string) {
|
||||
await queryClient.cancelQueries({ queryKey: taskKeys.detail(chatId) })
|
||||
const previous = queryClient.getQueryData<TaskChatHistory>(taskKeys.detail(chatId))
|
||||
if (previous) {
|
||||
const exists = previous.resources.some((r) => r.type === resource.type && r.id === resource.id)
|
||||
const exists = previous.resources.some(
|
||||
(r) => r.type === resource.type && r.id === resource.id
|
||||
)
|
||||
if (!exists) {
|
||||
queryClient.setQueryData<TaskChatHistory>(taskKeys.detail(chatId), {
|
||||
...previous,
|
||||
|
||||
@@ -11,10 +11,7 @@ const logger = createLogger('VfsTools')
|
||||
* Resolves a VFS resource path to its resource descriptor by reading the
|
||||
* sibling meta.json (already in memory) for the resource ID and name.
|
||||
*/
|
||||
function resolveVfsResource(
|
||||
vfs: WorkspaceVFS,
|
||||
path: string
|
||||
): MothershipResource | null {
|
||||
function resolveVfsResource(vfs: WorkspaceVFS, path: string): MothershipResource | null {
|
||||
const segments = path.split('/')
|
||||
const resourceType = VFS_DIR_TO_RESOURCE[segments[0]]
|
||||
if (!resourceType || !segments[1]) return null
|
||||
|
||||
@@ -5,8 +5,10 @@ import { eq, sql } from 'drizzle-orm'
|
||||
|
||||
const logger = createLogger('CopilotResources')
|
||||
|
||||
export type { MothershipResourceType as ResourceType } from '@/lib/copilot/resource-types'
|
||||
export type { MothershipResource as ChatResource } from '@/lib/copilot/resource-types'
|
||||
export type {
|
||||
MothershipResource as ChatResource,
|
||||
MothershipResourceType as ResourceType,
|
||||
} from '@/lib/copilot/resource-types'
|
||||
|
||||
const RESOURCE_TOOL_NAMES = new Set([
|
||||
'user_table',
|
||||
@@ -45,22 +47,29 @@ export function extractResourcesFromToolResult(
|
||||
case 'user_table': {
|
||||
if (result.tableId) {
|
||||
return [
|
||||
{ type: 'table', id: result.tableId as string, title: (result.tableName as string) || 'Table' },
|
||||
{
|
||||
type: 'table',
|
||||
id: result.tableId as string,
|
||||
title: (result.tableName as string) || 'Table',
|
||||
},
|
||||
]
|
||||
}
|
||||
if (result.fileId) {
|
||||
return [
|
||||
{ type: 'file', id: result.fileId as string, title: (result.fileName as string) || 'File' },
|
||||
{
|
||||
type: 'file',
|
||||
id: result.fileId as string,
|
||||
title: (result.fileName as string) || 'File',
|
||||
},
|
||||
]
|
||||
}
|
||||
const table = asRecord(data.table)
|
||||
if (table.id) {
|
||||
return [
|
||||
{ type: 'table', id: table.id as string, title: (table.name as string) || 'Table' },
|
||||
]
|
||||
return [{ type: 'table', id: table.id as string, title: (table.name as string) || 'Table' }]
|
||||
}
|
||||
const args = asRecord(params?.args)
|
||||
const tableId = (data.tableId as string) ?? (args.tableId as string) ?? (params?.tableId as string)
|
||||
const tableId =
|
||||
(data.tableId as string) ?? (args.tableId as string) ?? (params?.tableId as string)
|
||||
if (tableId) {
|
||||
return [
|
||||
{ type: 'table', id: tableId as string, title: (data.tableName as string) || 'Table' },
|
||||
@@ -72,9 +81,7 @@ export function extractResourcesFromToolResult(
|
||||
case 'workspace_file': {
|
||||
const file = asRecord(data.file)
|
||||
if (file.id) {
|
||||
return [
|
||||
{ type: 'file', id: file.id as string, title: (file.name as string) || 'File' },
|
||||
]
|
||||
return [{ type: 'file', id: file.id as string, title: (file.name as string) || 'File' }]
|
||||
}
|
||||
const fileId = (data.fileId as string) ?? (data.id as string)
|
||||
if (fileId) {
|
||||
@@ -88,12 +95,20 @@ export function extractResourcesFromToolResult(
|
||||
case 'read': {
|
||||
if (result.tableId) {
|
||||
return [
|
||||
{ type: 'table', id: result.tableId as string, title: (result.tableName as string) || 'Table' },
|
||||
{
|
||||
type: 'table',
|
||||
id: result.tableId as string,
|
||||
title: (result.tableName as string) || 'Table',
|
||||
},
|
||||
]
|
||||
}
|
||||
if (result.fileId) {
|
||||
return [
|
||||
{ type: 'file', id: result.fileId as string, title: (result.fileName as string) || 'File' },
|
||||
{
|
||||
type: 'file',
|
||||
id: result.fileId as string,
|
||||
title: (result.fileName as string) || 'File',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
@@ -101,10 +116,16 @@ export function extractResourcesFromToolResult(
|
||||
|
||||
case 'create_workflow':
|
||||
case 'edit_workflow': {
|
||||
const workflowId = (result.workflowId as string) ?? (data.workflowId as string) ?? (params?.workflowId as string)
|
||||
const workflowId =
|
||||
(result.workflowId as string) ??
|
||||
(data.workflowId as string) ??
|
||||
(params?.workflowId as string)
|
||||
if (workflowId) {
|
||||
const workflowName =
|
||||
(result.workflowName as string) ?? (data.workflowName as string) ?? (params?.workflowName as string) ?? 'Workflow'
|
||||
(result.workflowName as string) ??
|
||||
(data.workflowName as string) ??
|
||||
(params?.workflowName as string) ??
|
||||
'Workflow'
|
||||
return [{ type: 'workflow', id: workflowId, title: workflowName }]
|
||||
}
|
||||
return []
|
||||
@@ -112,9 +133,13 @@ export function extractResourcesFromToolResult(
|
||||
|
||||
case 'knowledge_base': {
|
||||
const kbId =
|
||||
(data.id as string) ?? (result.knowledgeBaseId as string) ?? (data.knowledgeBaseId as string) ?? (params?.knowledgeBaseId as string)
|
||||
(data.id as string) ??
|
||||
(result.knowledgeBaseId as string) ??
|
||||
(data.knowledgeBaseId as string) ??
|
||||
(params?.knowledgeBaseId as string)
|
||||
if (kbId) {
|
||||
const kbName = (data.name as string) ?? (result.knowledgeBaseName as string) ?? 'Knowledge Base'
|
||||
const kbName =
|
||||
(data.name as string) ?? (result.knowledgeBaseName as string) ?? 'Knowledge Base'
|
||||
return [{ type: 'knowledgebase', id: kbId, title: kbName }]
|
||||
}
|
||||
return []
|
||||
@@ -127,7 +152,11 @@ export function extractResourcesFromToolResult(
|
||||
for (const kb of kbArray) {
|
||||
const id = kb.id as string | undefined
|
||||
if (id) {
|
||||
resources.push({ type: 'knowledgebase', id, title: (kb.name as string) || 'Knowledge Base' })
|
||||
resources.push({
|
||||
type: 'knowledgebase',
|
||||
id,
|
||||
title: (kb.name as string) || 'Knowledge Base',
|
||||
})
|
||||
}
|
||||
}
|
||||
return resources
|
||||
@@ -142,7 +171,10 @@ export function extractResourcesFromToolResult(
|
||||
* Appends resources to a chat's JSONB resources column, deduplicating by type+id.
|
||||
* Updates the title of existing resources if the new title is more specific.
|
||||
*/
|
||||
export async function persistChatResources(chatId: string, newResources: ChatResource[]): Promise<void> {
|
||||
export async function persistChatResources(
|
||||
chatId: string,
|
||||
newResources: ChatResource[]
|
||||
): Promise<void> {
|
||||
if (newResources.length === 0) return
|
||||
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,6 @@ export const WORKSPACE_COLORS = [
|
||||
'#22c55e', // Green
|
||||
'#FFCC02', // Yellow
|
||||
'#a855f7', // Purple
|
||||
'#4aea7f', // Mint
|
||||
'#f97316', // Orange
|
||||
'#14b8a6', // Teal
|
||||
'#ff6b6b', // Coral
|
||||
|
||||
Reference in New Issue
Block a user