Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions webui/frontend/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
* rounded-msa-8 shadow-msa-s text-msa-sm
* ================================================================ */
@theme {
--animate-msa-session-dot: msa-session-dot 1.4s ease-in-out infinite;

/* --- Background --- */
--color-msa-bg-1: var(--msa-bg-1);
--color-msa-bg-2: var(--msa-bg-2);
Expand Down Expand Up @@ -371,6 +373,18 @@ a {
animation: msa-loading-reveal 150ms ease-out 250ms forwards;
}

@keyframes msa-session-dot {
0%,
60%,
100% {
opacity: 0.3;
}

30% {
opacity: 0.9;
}
}

@keyframes msa-agent-gaze-working {
0%,
8%,
Expand Down
1 change: 1 addition & 0 deletions webui/frontend/app/assets/icons/sliders.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 61 additions & 43 deletions webui/frontend/app/components/common/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import CaretDownIcon from '~/assets/icons/chevron-down.svg?react'
import AddIcon from '~/assets/icons/add.svg?react'
import FolderIcon from '~/assets/icons/folder.svg?react'
import SendIcon from '~/assets/icons/send.svg?react'
import MoreIcon from '~/assets/icons/more.svg?react'
import SlidersIcon from '~/assets/icons/sliders.svg?react'
import EditIcon from '~/assets/icons/edit.svg?react'

// Sender runs PERMANENTLY in slot mode (structured input): picked skills are
Expand Down Expand Up @@ -194,13 +194,14 @@ export function Composer({
// (container queries on the footer) so the first paint is correct at any width
// with no SSR/hydration flash; `pillsExpanded` only flips after a user click.
const [pillsExpanded, setPillsExpanded] = useState(false)
const pillsRef = useRef<HTMLDivElement>(null)
const pillsMouseDownRef = useRef<MouseEvent | null>(null)
useEffect(() => {
if (!pillsExpanded) return
const handleClickOutside = (e: MouseEvent) => {
if (pillsRef.current && !pillsRef.current.contains(e.target as Node)) {
if (pillsMouseDownRef.current !== e) {
setPillsExpanded(false)
}
pillsMouseDownRef.current = null
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
Expand Down Expand Up @@ -265,25 +266,41 @@ export function Composer({
const [globalSkills, setGlobalSkills] = useState<Skill[]>(
appData?.globalSkills ?? []
)
const [projectMcps, setProjectMcps] = useState<Mcp[]>([])
const [projectSkills, setProjectSkills] = useState<Skill[]>([])
const projectScope: Scope | null = effectiveProject
? `project:${effectiveProject.id}`
: null
const [projectMcps, setProjectMcps] = useState<{
scope: Scope
items: Mcp[]
} | null>(null)
const [projectSkills, setProjectSkills] = useState<{
scope: Scope
items: Skill[]
} | null>(null)
const [projectResourceRevision, setProjectResourceRevision] = useState(0)
useEffect(() => {
if (effectiveProject) {
const scope: Scope = `project:${effectiveProject.id}`
api
.listMcps(scope)
.then(setProjectMcps)
.catch(() => setProjectMcps([]))
api
.listSkills(scope)
.then(setProjectSkills)
.catch(() => setProjectSkills([]))
} else {
setProjectMcps([])
setProjectSkills([])
if (!projectScope) return
let active = true
api
.listMcps(projectScope)
.then((items) => {
if (active) setProjectMcps({ scope: projectScope, items })
})
.catch(() => {
if (active) setProjectMcps({ scope: projectScope, items: [] })
})
api
.listSkills(projectScope)
.then((items) => {
if (active) setProjectSkills({ scope: projectScope, items })
})
.catch(() => {
if (active) setProjectSkills({ scope: projectScope, items: [] })
})
return () => {
active = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [effectiveProject?.id])
}, [projectScope, projectResourceRevision])

// Re-fetch when MCP/Skill config changes on the same page (e.g. toggled in
// the project-detail MCPs tab while Composer is mounted above).
Expand All @@ -296,19 +313,8 @@ export function Composer({
.listSkills('global')
.then(setGlobalSkills)
.catch(() => setGlobalSkills([]))
if (effectiveProject) {
const scope: Scope = `project:${effectiveProject.id}`
api
.listMcps(scope)
.then(setProjectMcps)
.catch(() => setProjectMcps([]))
api
.listSkills(scope)
.then(setProjectSkills)
.catch(() => setProjectSkills([]))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [effectiveProject?.id])
setProjectResourceRevision((revision) => revision + 1)
}, [])
useOnMcpSkillChanged(refreshMcpSkill)

// Re-fetch when the model catalog changes elsewhere (Settings → Models, or an
Expand Down Expand Up @@ -356,12 +362,18 @@ export function Composer({
useOnProjectsChanged(refreshProjects)

const mergedMcps = useMemo(
() => [...globalMcps, ...projectMcps],
[globalMcps, projectMcps]
() => [
...globalMcps,
...(projectMcps?.scope === projectScope ? projectMcps.items : [])
],
[globalMcps, projectMcps, projectScope]
)
const mergedSkills = useMemo(
() => [...globalSkills, ...projectSkills],
[globalSkills, projectSkills]
() => [
...globalSkills,
...(projectSkills?.scope === projectScope ? projectSkills.items : [])
],
[globalSkills, projectSkills, projectScope]
)

// Web-search config is GLOBAL, so it is resolved once by the app layout's
Expand Down Expand Up @@ -1408,7 +1420,10 @@ export function Composer({
`min-w-24` floor and truncate the label instead of spilling
onto a second line. */}
<div
ref={pillsRef}
onMouseDownCapture={(e) => {
// React capture includes portaled menus outside the DOM subtree.
pillsMouseDownRef.current = e.nativeEvent
}}
className={`flex items-center gap-2.5 ${
pillsExpanded
? 'absolute inset-x-0 bottom-0 z-10 min-w-0 flex-nowrap overflow-x-auto bg-msa-bg-1 pt-3 @min-[550px]:static @min-[550px]:overflow-x-visible @min-[550px]:bg-transparent @min-[550px]:pt-0'
Expand All @@ -1417,11 +1432,14 @@ export function Composer({
>
{/* Toggle button: shown only while collapsed */}
{!pillsExpanded && (
<IconButton
className="@min-[550px]:hidden"
icon={<MoreIcon className="h-5 w-5" />}
onClick={() => setPillsExpanded(true)}
/>
<Tooltip title={t.nav.expand}>
<IconButton
aria-label={t.nav.expand}
className="@min-[550px]:hidden"
icon={<SlidersIcon className="h-5 w-5" />}
onClick={() => setPillsExpanded(true)}
/>
</Tooltip>
)}

<div
Expand Down
22 changes: 16 additions & 6 deletions webui/frontend/app/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import NewProjectIcon from '~/assets/icons/new-project.svg?react'
import MoreIcon from '~/assets/icons/more.svg?react'
import ExpandIcon from '~/assets/icons/expand.svg?react'
import GithubIcon from '~/assets/icons/github.svg?react'
import SpinnerIcon from '~/assets/icons/generating.svg?react'

const REPO_URL = 'https://github.com/modelscope/ms-agent'

Expand Down Expand Up @@ -1059,13 +1058,24 @@ function SessionItem({
strands the indicator against the right edge, far from the words it
refers to. The wrapper still absorbs the same slack, so the more-menu
stays pinned right. */}
<span className="flex min-w-0 flex-1 items-center gap-1">
<span className="flex min-w-0 flex-1 items-center gap-2">
<span className="min-w-0 truncate">{session.title}</span>
{/* Spinner while the turn runs, dot once it has finished unwatched —
the same slot, never both: the dot exists precisely because the
spinner went away with nothing left to say the answer arrived. */}
{isRunning ? (
<SpinnerIcon className="h-3 w-3 shrink-0 animate-spin text-msa-text-brand1" />
<span
role="img"
aria-label={t.chat.authExecuting}
className="flex h-3 w-3 shrink-0 items-center justify-between text-msa-text-brand1"
>
<span
className="h-[3px] w-[3px] rounded-full bg-current opacity-60 motion-safe:animate-msa-session-dot"
style={{ animationDelay: '-0.32s' }}
/>
<span
className="h-[3px] w-[3px] rounded-full bg-current opacity-60 motion-safe:animate-msa-session-dot"
style={{ animationDelay: '-0.16s' }}
/>
<span className="h-[3px] w-[3px] rounded-full bg-current opacity-60 motion-safe:animate-msa-session-dot" />
</span>
) : session.unread ? (
<UnreadDot spinnerSlot label={t.sidebar.unreadSession} />
) : null}
Expand Down
36 changes: 29 additions & 7 deletions webui/frontend/app/components/messages/ForkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { useT } from '~/lib/i18n'
import type { ForkOrigin } from '~/lib/types'
import { IconButton } from '../common/IconButton'

export function ForkReplyButton({ sessionId, afterSeq }: {
export function ForkReplyButton({
sessionId,
afterSeq
}: {
sessionId: string
afterSeq: number
}) {
Expand All @@ -32,9 +35,16 @@ export function ForkReplyButton({ sessionId, afterSeq }: {
request.current = { seq: afterSeq, id: clientId() }
}
try {
const child = await api.forkSession(sessionId, afterSeq, request.current.id)
const child = await api.forkSession(
sessionId,
afterSeq,
request.current.id
)
revalidator.revalidate()
navigate(`/projects/${encodeURIComponent(child.project_id!)}/sessions/${encodeURIComponent(child.id)}`, { state: { focusComposer: true } })
navigate(
`/projects/${encodeURIComponent(child.project_id!)}/sessions/${encodeURIComponent(child.id)}`,
{ state: { focusComposer: true } }
)
} catch (error) {
if (!(error instanceof ApiError)) message.error(t.chat.forkFailed)
} finally {
Expand All @@ -47,7 +57,7 @@ export function ForkReplyButton({ sessionId, afterSeq }: {
<IconButton
size="sm"
aria-label={t.chat.forkReply}
icon={<BranchesOutlined />}
icon={<BranchesOutlined className="text-sm" />}
loading={pending}
disabled={pending}
onClick={() => void fork()}
Expand All @@ -61,15 +71,27 @@ export function ForkDivider({ origin }: { origin: ForkOrigin }) {
const { t } = useT()
const label = t.chat.forkFrom.replace('{title}', origin.title)
return (
<div className="my-4 flex w-full items-center gap-3 text-xs text-msa-text-3" data-fork-origin={origin.session_id}>
<div
className="my-4 flex w-full items-center gap-3 text-xs text-msa-text-3"
data-fork-origin={origin.session_id}
>
<span className="h-px min-w-4 flex-1 bg-msa-line-1" />
{origin.available ? (
<Link
className="max-w-[75%] truncate text-msa-text-3 hover:text-msa-text-brand1"
title={label}
to={`/projects/${encodeURIComponent(origin.project_id)}/sessions/${encodeURIComponent(origin.session_id)}?at=${origin.assistant_seq}`}
>{label}</Link>
) : <span className="max-w-[75%] truncate" title={`${label} · ${t.chat.forkSourceDeleted}`}>{label} · {t.chat.forkSourceDeleted}</span>}
>
{label}
</Link>
) : (
<span
className="max-w-[75%] truncate"
title={`${label} · ${t.chat.forkSourceDeleted}`}
>
{label} · {t.chat.forkSourceDeleted}
</span>
)}
<span className="h-px min-w-4 flex-1 bg-msa-line-1" />
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion webui/frontend/app/components/models/ProviderLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function placeholderColor(seed: string): string {

/** First letter/digit of the name, for the placeholder tile. */
function initial(text: string): string {
const c = [...text].find((ch) => /[A-Za-z0-9]/.test(ch))
const c = [...text].find((ch) => /[\p{L}\p{N}]/u.test(ch))
return (c ?? '?').toUpperCase()
}

Expand Down
6 changes: 4 additions & 2 deletions webui/frontend/app/lib/sessionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ export function useSessionModel(sessionId: string | null, initialId: string, ena
if (sessionId) {
try { saved.current = (await api.getSession(sessionId, { silent: true })).model_id || '' } catch { /* keep last known selection */ }
}
selected.current = saved.current
setModelId(saved.current)
if (revision.current === version) {
selected.current = saved.current
setModelId(saved.current)
}
}
throw failure
}).finally(() => { saving.current -= 1 })
Expand Down
2 changes: 1 addition & 1 deletion webui/frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"**/.client/**/*",
".react-router/types/**/*"
],
"exclude": ["node_modules", "build", "public", "backend"],
"exclude": ["node_modules", "build-runtime", "build", "public", "backend"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
Expand Down