Skip to content

Commit bbbd80c

Browse files
committed
fix(chat): open workflow folder tabs in Workflows, link tests only into a shown Tests tab, leave generic cards unlinked, and drop file folders from the panel picker
1 parent dedb2dd commit bbbd80c

4 files changed

Lines changed: 194 additions & 88 deletions

File tree

‎apps/sim/platform/lib/data/chat-mentions.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ export function useDashboardsShown(): boolean {
133133
return isWorkspaceTabShown('dashboards', { dashboards, 'platform-preview': preview })
134134
}
135135

136+
/** Whether the Tests tab is shown, which decides whether a test the chat opened links into it. */
137+
export function useTestsShown(): boolean {
138+
const tests = useFeatureFlag('workflow-tests')
139+
const preview = useFeatureFlag('platform-preview')
140+
return isWorkspaceTabShown('tests', { 'workflow-tests': tests, 'platform-preview': preview })
141+
}
142+
136143
/**
137144
* Mention rows as the pickers list them, in groups by family. A dashboard is offered only while the
138145
* Dashboard tab it opens into is shown.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Failure modes: a chat panel tab's Open link landing on the wrong surface (a workflow folder in
5+
* Files), or looping back to the chat it sits in (a test or a generic card linked to Home with
6+
* `?resource=`), or linking into a Tests tab the viewer cannot see; and the + picker offering a
7+
* kind whose tab has no view, so the pick opens an empty card.
8+
*/
9+
import { describe, expect, it } from 'vitest'
10+
import type { MothershipResource } from '@/lib/mothership/resources/types'
11+
import {
12+
isPanelFamily,
13+
type PanelTabsShown,
14+
pickedResource,
15+
resourceHref,
16+
} from '@/platform/lib/data/chat-panel-model'
17+
18+
const scope = { routePrefix: '/o/org-1' }
19+
const allShown: PanelTabsShown = { dashboards: true, tests: true }
20+
21+
function href(resource: Omit<MothershipResource, 'title'>, shown = allShown) {
22+
return resourceHref(scope, { title: 'Resource', ...resource }, shown)
23+
}
24+
25+
describe('resourceHref', () => {
26+
it('opens a workflow folder in the workflows list, filtered to it', () => {
27+
expect(href({ type: 'folder', id: 'folder-1', workspaceId: 'ws-1' })).toBe(
28+
'/workspace/ws-1/workflows?folderId=folder-1'
29+
)
30+
})
31+
32+
it('still opens a file folder in Files', () => {
33+
expect(href({ type: 'filefolder', id: 'files-1', workspaceId: 'ws-1' })).toBe(
34+
'/workspace/ws-1/files?folderId=files-1'
35+
)
36+
})
37+
38+
it('opens a test on its Tests page while the Tests tab is shown', () => {
39+
expect(href({ type: 'test', id: 'checkout flow', workspaceId: 'ws-1' })).toBe(
40+
'/workspace/ws-1/tests/checkout%20flow'
41+
)
42+
})
43+
44+
it('gives a test no link while the Tests tab is hidden', () => {
45+
expect(
46+
href({ type: 'test', id: 'checkout', workspaceId: 'ws-1' }, { ...allShown, tests: false })
47+
).toBeUndefined()
48+
})
49+
50+
it('gives a generic card no link back into the chat', () => {
51+
expect(href({ type: 'generic', id: 'note', workspaceId: 'ws-1' })).toBeUndefined()
52+
})
53+
})
54+
55+
describe('panel picker', () => {
56+
it('offers no file folders, which have no panel view', () => {
57+
expect(isPanelFamily('file_folders')).toBe(false)
58+
expect(isPanelFamily('files')).toBe(true)
59+
})
60+
61+
it('opens no tab for a picked file folder', () => {
62+
expect(
63+
pickedResource(
64+
{ kind: 'filefolder', fileFolderId: 'files-1', label: 'Specs', workspaceId: 'ws-1' },
65+
new Map()
66+
)
67+
).toBeNull()
68+
})
69+
})
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { MentionFamily } from '@/lib/api/contracts/organization-mentions'
2+
import type { MothershipResource } from '@/lib/mothership/resources/types'
3+
import { workspacePath } from '@/lib/navigation/paths'
4+
import { isWorkspaceResourceKind, workspaceResourcePath } from '@/lib/resources'
5+
import type { ChatContext } from '@/platform/lib/data/chat-types'
6+
import { type SurfaceScope, surfaceHref } from '@/platform/lib/data/organization-model'
7+
8+
const RESOURCE_KIND: Partial<Record<string, string>> = {
9+
knowledgebase: 'knowledge',
10+
filefolder: 'folder',
11+
}
12+
13+
/** The workspace tabs held behind a flag that a panel tab can link into. */
14+
export interface PanelTabsShown {
15+
dashboards: boolean
16+
tests: boolean
17+
}
18+
19+
/**
20+
* A workspace resource opens on its workspace page; any other opens beside the organization's Home.
21+
* A workflow folder opens the workflows list inside it. A dashboard or a test links into its tab
22+
* only while that tab is shown, and a generic card, which only the chat shows, links nowhere.
23+
*/
24+
export function resourceHref(
25+
scope: SurfaceScope,
26+
resource: MothershipResource,
27+
shown: PanelTabsShown
28+
): string | undefined {
29+
if (!resource.workspaceId || resource.type === 'generic') return undefined
30+
if (resource.type === 'folder')
31+
return `${workspacePath(resource.workspaceId, 'workflows')}?${new URLSearchParams({ folderId: resource.id })}`
32+
const kind = RESOURCE_KIND[resource.type] ?? resource.type
33+
if (isWorkspaceResourceKind(kind))
34+
return workspaceResourcePath(resource.workspaceId, kind, resource.id)
35+
if (kind === 'dashboard')
36+
return shown.dashboards ? workspacePath(resource.workspaceId, 'dashboards') : undefined
37+
if (kind === 'test')
38+
return shown.tests
39+
? workspacePath(resource.workspaceId, `tests/${encodeURIComponent(resource.id)}`)
40+
: undefined
41+
if (kind === 'log' && resource.executionId)
42+
return workspacePath(resource.workspaceId, `logs/${encodeURIComponent(resource.executionId)}`)
43+
return `${surfaceHref(scope, '/home')}?${new URLSearchParams({ resource: resource.id })}`
44+
}
45+
46+
/** A kind that belongs to one workspace, which a workspace chat's older tabs leave implied. */
47+
export function livesInWorkspace(type: string): boolean {
48+
const kind = RESOURCE_KIND[type] ?? type
49+
return isWorkspaceResourceKind(kind) || kind === 'dashboard' || kind === 'log'
50+
}
51+
52+
/**
53+
* The families a panel tab shows, as its + picker offers them. Chats, skills and workflow, table
54+
* and knowledge base folders open no tab of their own, as in legacy's picker, and neither do file
55+
* folders, which no panel view renders. Dashboards are offered only while the Dashboard tab is
56+
* shown, which `grouped` decides.
57+
*/
58+
const PANEL_FAMILIES: ReadonlySet<MentionFamily> = new Set([
59+
'dashboards',
60+
'tables',
61+
'files',
62+
'knowledge_bases',
63+
'workflows',
64+
'logs',
65+
])
66+
67+
/** Whether the panel's + picker offers rows of `family`. */
68+
export function isPanelFamily(family: MentionFamily): boolean {
69+
return PANEL_FAMILIES.has(family)
70+
}
71+
72+
/** The tab a picked reference opens, or nothing for a kind no tab shows. */
73+
export function pickedResource(
74+
context: ChatContext,
75+
logIds: ReadonlyMap<string, string>
76+
): MothershipResource | null {
77+
const owned = {
78+
title: context.label,
79+
workspaceId: 'workspaceId' in context ? context.workspaceId : undefined,
80+
}
81+
switch (context.kind) {
82+
case 'table':
83+
return { ...owned, type: 'table', id: context.tableId }
84+
case 'file':
85+
return { ...owned, type: 'file', id: context.fileId }
86+
case 'knowledge':
87+
return context.knowledgeId
88+
? { ...owned, type: 'knowledgebase', id: context.knowledgeId }
89+
: null
90+
case 'workflow':
91+
return { ...owned, type: 'workflow', id: context.workflowId }
92+
case 'dashboard':
93+
return { ...owned, type: 'dashboard', id: context.dashboardId }
94+
case 'logs': {
95+
const id = context.executionId && logIds.get(context.executionId)
96+
return id ? { ...owned, type: 'log', id, executionId: context.executionId } : null
97+
}
98+
default:
99+
return null
100+
}
101+
}

‎apps/sim/platform/lib/data/chat-panel.ts‎

Lines changed: 17 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33
import { useEffect, useState } from 'react'
44
import { resolveFileCategory } from '@/components/file-viewer/file-category'
5-
import { MENTION_BROWSE_LIMIT, type MentionFamily } from '@/lib/api/contracts/organization-mentions'
5+
import { MENTION_BROWSE_LIMIT } from '@/lib/api/contracts/organization-mentions'
66
import type { MothershipResource } from '@/lib/mothership/resources/types'
7-
import { workspacePath } from '@/lib/navigation/paths'
8-
import { isWorkspaceResourceKind, workspaceResourcePath } from '@/lib/resources'
97
import { requestTerminalFocus } from '@/lib/terminal/focus'
108
import { terminalIdFromResourceId } from '@/lib/terminal/resource-id'
119
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file'
@@ -18,8 +16,18 @@ import type {
1816
} from '@/hooks/mothership-chat/use-resource-panel'
1917
import { useTerminalTabResources } from '@/hooks/mothership-chat/use-terminal-tab-resources'
2018
import { useOrganizationMentions } from '@/hooks/queries/organization-mentions'
21-
import { grouped, useDashboardsShown, useMentionBrowse } from '@/platform/lib/data/chat-mentions'
22-
import type { ChatContext } from '@/platform/lib/data/chat-types'
19+
import {
20+
grouped,
21+
useDashboardsShown,
22+
useMentionBrowse,
23+
useTestsShown,
24+
} from '@/platform/lib/data/chat-mentions'
25+
import {
26+
isPanelFamily,
27+
livesInWorkspace,
28+
pickedResource,
29+
resourceHref,
30+
} from '@/platform/lib/data/chat-panel-model'
2331
import type {
2432
ChatSource,
2533
FileStream,
@@ -28,38 +36,6 @@ import type {
2836
} from '@/platform/lib/data/models/chat'
2937
import type { PanelPicker, PanelResources } from '@/platform/lib/data/models/chat-panel'
3038
import { useOrganization } from '@/platform/lib/data/organization'
31-
import { type SurfaceScope, surfaceHref } from '@/platform/lib/data/organization-model'
32-
33-
const RESOURCE_KIND: Partial<Record<string, string>> = {
34-
knowledgebase: 'knowledge',
35-
filefolder: 'folder',
36-
}
37-
38-
/**
39-
* A workspace resource opens on its workspace page; any other opens beside the organization's Home.
40-
* A dashboard links to the Dashboard tab only while that tab is shown.
41-
*/
42-
function resourceHref(
43-
scope: SurfaceScope,
44-
resource: MothershipResource,
45-
dashboardsShown: boolean
46-
): string | undefined {
47-
if (!resource.workspaceId) return undefined
48-
const kind = RESOURCE_KIND[resource.type] ?? resource.type
49-
if (isWorkspaceResourceKind(kind))
50-
return workspaceResourcePath(resource.workspaceId, kind, resource.id)
51-
if (kind === 'dashboard')
52-
return dashboardsShown ? workspacePath(resource.workspaceId, 'dashboards') : undefined
53-
if (kind === 'log' && resource.executionId)
54-
return workspacePath(resource.workspaceId, `logs/${encodeURIComponent(resource.executionId)}`)
55-
return `${surfaceHref(scope, '/home')}?${new URLSearchParams({ resource: resource.id })}`
56-
}
57-
58-
/** A kind that belongs to one workspace, which a workspace chat's older tabs leave implied. */
59-
function livesInWorkspace(type: string): boolean {
60-
const kind = RESOURCE_KIND[type] ?? type
61-
return isWorkspaceResourceKind(kind) || kind === 'dashboard' || kind === 'log'
62-
}
6339

6440
/** A Sources tab lists what its answer cited; the transcript already holds that evidence. */
6541
function citedBy(
@@ -184,6 +160,7 @@ export function usePanelResources({
184160
useTerminalTabResources(panel.desktopTabResourceOptions)
185161
const scope = useOrganization()
186162
const dashboardsShown = useDashboardsShown()
163+
const testsShown = useTestsShown()
187164
const picker = usePanelPicker(panel, dashboardsShown)
188165
const locked = useAgentFileEditLock(chat.previewSession?.status === 'streaming', chat.isSending)
189166
const resources: PlatformResource[] = chat.resources.map((stored) => {
@@ -196,7 +173,7 @@ export function usePanelResources({
196173
type: resource.type,
197174
title: resource.title,
198175
workspaceId: resource.workspaceId,
199-
href: resourceHref(scope, resource, dashboardsShown),
176+
href: resourceHref(scope, resource, { dashboards: dashboardsShown, tests: testsShown }),
200177
executionId: resource.executionId,
201178
active: resource.id === chat.activeResourceId,
202179
search: resource.search,
@@ -231,54 +208,6 @@ export function usePanelResources({
231208
}
232209
}
233210

234-
/**
235-
* The families a panel tab shows, as its + picker offers them. Chats, skills and workflow, table
236-
* and knowledge base folders open no tab of their own, as in legacy's picker. Dashboards are offered
237-
* only while the Dashboard tab is shown, which `grouped` decides.
238-
*/
239-
const PANEL_FAMILIES: ReadonlySet<MentionFamily> = new Set([
240-
'dashboards',
241-
'tables',
242-
'files',
243-
'file_folders',
244-
'knowledge_bases',
245-
'workflows',
246-
'logs',
247-
])
248-
249-
/** The tab a picked reference opens, or nothing for a kind no tab shows. */
250-
function pickedResource(
251-
context: ChatContext,
252-
logIds: ReadonlyMap<string, string>
253-
): MothershipResource | null {
254-
const owned = {
255-
title: context.label,
256-
workspaceId: 'workspaceId' in context ? context.workspaceId : undefined,
257-
}
258-
switch (context.kind) {
259-
case 'table':
260-
return { ...owned, type: 'table', id: context.tableId }
261-
case 'file':
262-
return { ...owned, type: 'file', id: context.fileId }
263-
case 'filefolder':
264-
return { ...owned, type: 'filefolder', id: context.fileFolderId }
265-
case 'knowledge':
266-
return context.knowledgeId
267-
? { ...owned, type: 'knowledgebase', id: context.knowledgeId }
268-
: null
269-
case 'workflow':
270-
return { ...owned, type: 'workflow', id: context.workflowId }
271-
case 'dashboard':
272-
return { ...owned, type: 'dashboard', id: context.dashboardId }
273-
case 'logs': {
274-
const id = context.executionId && logIds.get(context.executionId)
275-
return id ? { ...owned, type: 'log', id, executionId: context.executionId } : null
276-
}
277-
default:
278-
return null
279-
}
280-
}
281-
282211
/**
283212
* The panel's + picker over the organization's mention reads: each workspace with its most recently
284213
* updated tables, files, knowledge bases, workflows, logs and (while their tab is shown) dashboards,
@@ -299,10 +228,10 @@ function usePanelPicker(
299228
{ enabled: enabled && !browse.searching }
300229
)
301230
const rows = browse.searching
302-
? browse.rows.filter((row) => PANEL_FAMILIES.has(row.family))
231+
? browse.rows.filter((row) => isPanelFamily(row.family))
303232
: [
304233
...browse.rows.filter((row) => row.family === 'workspaces'),
305-
...(recent.data?.rows ?? []).filter((row) => PANEL_FAMILIES.has(row.family)),
234+
...(recent.data?.rows ?? []).filter((row) => isPanelFamily(row.family)),
306235
]
307236
const logIds = new Map(
308237
rows.flatMap((row) =>

0 commit comments

Comments
 (0)