fix(web): show plan status only while active

This commit is contained in:
kingwl
2026-07-30 22:42:25 +08:00
parent 2e466dc649
commit 1986de9ac1
30 changed files with 212 additions and 157 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-plan/README.md
README.md: 1d22c057b439ff337bf9daadcdba96dd4cca4540
README.zh.md: 183b8ef7776b60c1f0afa630e04627a474d40391
README.md: 2f83dd738e11ada5b24a55d1eec97e154ffd2aea
README.zh.md: a026390191179f6492a55ee931a3406d8e1953e8

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Plan-mode status chip, a pure browser surface plugin. The browser half occupies the conversation-declared `conversation.input.plan` single seat (to the right of the access-mode control); the node half is an empty apply (the roster row). Plan behavior itself — the `/plan` command, the boundary-or-idle-committed `plan/mode` state, the `plan` projection unit, and the policy section — is owned by [`@deepseek-ai/dsh-plan-mode`](../../plan/plan-mode/README.md), composed independently on the host roster.
Plan mode is entered through the `/plan` command only; there is no UI control that turns it on. While the host-computed `plan` projection's effective target is plan mode (`pending ? !active : active` — a folded host value, not client optimism, so an arriving frame corrects the chip either way), the seat renders a read-only "Plan" chip whose hover × executes `/plan off` through `command.execute`; otherwise the seat stays empty — a host without plan-mode (or a Draft with no session) shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan", localized through ui-conversation's `command.hint` locale namespace and shared verbatim with the claimed `/plan` command hint (rendered by the composer from the same projection; owner-supplied placeholders win).
Plan mode is entered through the `/plan` command path: users can choose Plan from the composer's `+` Command menu or type `/plan`, while this package renders no inactive plan control. While the host-computed `plan` projection's effective target is plan mode (`pending ? !active : active` — a folded host value, not client optimism, so an arriving frame corrects the chip either way), the seat renders the warn-colored "Plan ×" status button, which executes `/plan off` through `command.execute`; otherwise the seat stays empty — a host without plan-mode (or a Draft with no session) shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan", localized through ui-conversation's `command.hint` locale namespace and shared verbatim with the claimed `/plan` command hint (rendered by the composer from the same projection; owner-supplied placeholders win).
The chip carries the accessible description "Plan mode on, press to turn off". Admission failures (`matched: false`, business errors, transport faults) surface as an inline error and the chip stays until the projection confirms the exit.
@@ -22,4 +22,4 @@ Entering or leaving plan mode changes the active `plan:policy` system-prompt sec
- **Plan mode is guidance, not an execution sandbox** — deployments that require enforced read-only planning must compose the independent sandbox and approval policies.
- **The chip belongs to the default composer** — a pending whole-composer interaction such as plan review temporarily replaces the InputBar and its chip.
- **No UI entry point** — plan mode is entered by typing `/plan`; a session with the capability but inactive mode shows no affordance in the tool row.
- **No inactive plan control** — entry uses the shared Command source; a session with the capability but inactive mode shows no plan affordance in the tool row.

View File

@@ -4,7 +4,7 @@
Plan mode 状态徽章,纯浏览器 surface 插件。浏览器侧占据会话声明的 `conversation.input.plan` 单座(位于 access 模式控件右侧node 侧是空 applyroster 行。plan 行为本身——`/plan` 命令、边界或空闲即时提交的 `plan/mode` 状态、`plan` 投影单元与 policy 段——归 [`@deepseek-ai/dsh-plan-mode`](../../plan/plan-mode/README.md) 所有,由 host roster 独立组合。
plan mode `/plan` 命令进入UI 上没有打开它的控件。当 host 计算的 `plan` 投影有效目标为 plan mode 时(`pending ? !active : active`——折叠的 host 值而非客户端乐观态,帧到达即自动纠正),座位渲染一个只读 "Plan" chiphover 出现的 × `command.execute` 执行 `/plan off`;否则座位保持为空——未组合 plan-mode 的 host或尚无会话的 Draft不显示任何内容。plan mode 为有效目标期间composer 文本框的 placeholder 切换为 plan 任务提示——"describe your task to generate plan"(中文「描述你的任务以生成计划」),经 ui-conversation 的 `command.hint` locale 命名空间本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(由 composer 从同一投影渲染owner 提供的 placeholder 优先)。
plan mode 经 `/plan` 命令路径进入:用户可以从 composer 的 `+` Command 菜单选择 Plan也可以输入 `/plan`而本包package不渲染未激活态 plan 控件。当 host 计算的 `plan` 投影有效目标为 plan mode 时(`pending ? !active : active`——折叠的 host 值而非客户端乐观态,帧到达即自动纠正),座位渲染 warn 色的 "Plan ×" 状态按钮,该按钮`command.execute` 执行 `/plan off`;否则座位保持为空——未组合 plan-mode 的 host或尚无会话的 Draft不显示任何内容。plan mode 为有效目标期间composer 文本框的 placeholder 切换为 plan 任务提示——"describe your task to generate plan"(中文「描述你的任务以生成计划」),经 ui-conversation 的 `command.hint` locale 命名空间本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(由 composer 从同一投影渲染owner 提供的 placeholder 优先)。
chip 携带无障碍描述 "Plan mode on, press to turn off"。准入失败(`matched: false`、业务错误、传输故障以内联错误呈现chip 保持显示直至投影确认退出。
@@ -22,4 +22,4 @@ chip 携带无障碍描述 "Plan mode on, press to turn off"。准入失败(`m
- **Plan mode 是引导而非执行沙箱**——需要强制只读规划的部署必须组合独立的沙箱与审批策略。
- **chip 属于默认编辑器**——待处理的整编辑器交互(如 plan 评审)会临时取代 InputBar 及其 chip。
- **无 UI 进入点**——plan mode 靠敲 `/plan` 进入;有能力但未激活的会话在工具行不显示任何入口。
- **无未激活态 plan 控件**——入口使用共享 Command source有能力但 mode 未激活的会话在工具行不显示 plan 入口。

View File

@@ -38,6 +38,7 @@
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-plan-mode": "^0.0.1",
@@ -48,6 +49,7 @@
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-client-web-react": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -1,5 +1,4 @@
/* Plan-mode toggle chip: quiet while off; the pressed state takes the
business accent pair (same token pairing as the trajectory user badge). */
/* Active plan status follows Figma's warn-state pill. */
.wrap {
display: inline-flex;
@@ -10,30 +9,25 @@
.chip {
display: inline-flex;
align-items: center;
padding: 4px 8px;
gap: 4px;
min-width: 34px;
padding: 2px 8px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--dsw-alias-label-secondary);
font-size: 14px;
border-radius: 999px;
background: var(--dsw-alias-state-warn-tertiary);
color: var(--dsw-alias-state-warn-label);
font-size: 13px;
font-weight: 500;
line-height: 20px;
cursor: pointer;
}
.chip:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
}
/* Hovering keeps the pressed accent: the higher-specificity hover rule above
would otherwise swap it back to the neutral hover wash. */
.chip[aria-pressed='true'],
.chip[aria-pressed='true']:hover:not(:disabled) {
color: var(--dsw-alias-state-business-primary);
background: var(--dsw-alias-state-business-tertiary);
color: var(--dsw-alias-state-warn-primary);
}
.chip:focus-visible {
outline: 2px solid var(--dsw-alias-label-secondary);
outline: 2px solid var(--dsw-alias-state-warn-label);
outline-offset: 2px;
}
@@ -42,6 +36,12 @@
cursor: default;
}
.close {
display: inline-flex;
align-items: center;
color: currentColor;
}
.error {
color: var(--dsw-alias-state-error-primary);
font-size: 12px;

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconCloseFill14 } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: pulls the ui-conversation SlotMap merge (the input.plan seat and
// its {locked} owner share).
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -11,16 +12,14 @@ export type PlanChipProps =
PropsRuntime<'conversation.input.plan'> & InjectFace<PlanChipInjected>
/**
* Plan-mode toggle over the host-computed `plan` projection. The chip renders
* whenever the capability is present and reflects the effective target as its
* pressed state (`pending ? !active : active` — a folded host value, not
* client optimism, so an arriving frame corrects it). Clicking executes
* /plan or /plan off toward the opposite target.
* Plan-mode status over the host-computed `plan` projection. The chip renders
* only while the effective target is plan mode (`pending ? !active : active`
* — a folded host value, not client optimism) and executes /plan off.
*/
export function PlanChip({ useProjection, locked, setPlanMode }: PlanChipProps) {
export function PlanChip({ useProjection, locked, exitPlanMode }: PlanChipProps) {
const plan = useProjection('plan')
const [busy, setBusy] = useState(false)
const [error, setError] = useState<{ text: string; detail: string } | null>(null)
const [leaving, setLeaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const aliveRef = useRef(true)
useEffect(() => {
@@ -30,25 +29,21 @@ export function PlanChip({ useProjection, locked, setPlanMode }: PlanChipProps)
}
}, [])
// Absent capability (no plan-mode host plugin / no session yet): no seat
// content — without the capability there is nothing to toggle.
if (plan === undefined) return null
const target = plan.pending ? !plan.active : plan.active
if (!target) return null
const toggle = (): void => {
// No busy/locked guard: both disable the button, so no click arrives.
const on = !target
const failText = on ? '进入 plan mode 失败' : '退出 plan mode 失败'
setBusy(true)
const off = (): void => {
setLeaving(true)
setError(null)
void setPlanMode(on).then((failure) => {
void exitPlanMode().then((failure) => {
if (!aliveRef.current) return
setBusy(false)
setError(failure === null ? null : { text: failText, detail: failure })
setLeaving(false)
setError(failure)
}, (reason: unknown) => {
if (!aliveRef.current) return
setBusy(false)
setError({ text: failText, detail: reason instanceof Error ? reason.message : String(reason) })
setLeaving(false)
setError(reason instanceof Error ? reason.message : String(reason))
})
}
@@ -57,17 +52,17 @@ export function PlanChip({ useProjection, locked, setPlanMode }: PlanChipProps)
<button
type="button"
className={css.chip}
aria-pressed={target}
aria-label={target ? 'Plan mode on, press to turn off' : 'Plan mode off, press to turn on'}
title={target
? 'Plan mode on — click to turn off (/plan off)'
: 'Plan mode off — click to turn on (/plan)'}
disabled={locked || busy}
onClick={toggle}
aria-label="Plan mode on, press to turn off"
title="Plan mode on — click to turn off (/plan off)"
disabled={locked || leaving}
onClick={off}
>
Plan { target ? 'on' : 'off' }
Plan
<span className={css.close} aria-hidden>
<IconCloseFill14 size={12} />
</span>
</button>
{error !== null && <span className={css.error} role="status" title={error.detail}>{error.text}</span>}
{error !== null && <span className={css.error} role="status" title={error}>退 plan mode </span>}
</span>
)
}

View File

@@ -1,11 +1,11 @@
/**
* Plan control plugin, browser half: occupies the composer's named
* `conversation.input.plan` seat with a plan-mode toggle chip. While the
* `plan` projection is present the chip renders in both states and executes
* /plan or /plan off through `command.execute` toward the opposite target;
* an absent projection (no capability) leaves the seat empty. Reads ride the
* generic projection pair through the standard-kit `useProjection` (an absent
* key is capability absence); zero client-side plan state.
* `conversation.input.plan` seat with an active-state status chip. Plan mode
* is entered through the command source; while the projection's effective
* target is plan mode the chip renders and executes /plan off through
* `command.execute`, otherwise the seat stays empty. Reads ride the generic
* projection pair through the standard-kit `useProjection`; zero client-side
* plan state.
*/
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
@@ -18,11 +18,10 @@ import { PlanChip } from './PlanModeControl.tsx'
/** Injected business face of the composer plan seat. */
export interface PlanChipInjected {
/**
* Switch plan mode by executing /plan (on) or /plan off.
* @param on - desired target: true enters plan mode, false leaves it.
* Leave plan mode by executing /plan off.
* @returns null on admitted execution; a user-visible failure line otherwise.
*/
setPlanMode: (on: boolean) => Promise<string | null>
exitPlanMode: () => Promise<string | null>
}
/**
@@ -39,12 +38,11 @@ export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.slots.register({
name: 'conversation.input.plan',
inject: (sessionId: SessionId): PlanChipInjected => ({
setPlanMode: async (on) => {
const line = on ? '/plan' : '/plan off'
exitPlanMode: async () => {
const connection = ctx.get('connection') as ConnectionHandle
const { result } = await connection.api.commands.execute({ sessionId, line })
const { result } = await connection.api.commands.execute({ sessionId, line: '/plan off' })
if (!result.ok) return `${result.error.message}${result.error.code}`
if (!result.value.matched) return `未知命令:${line}`
if (!result.value.matched) return '未知命令:/plan off'
return null
},
}),

View File

@@ -1,9 +1,9 @@
/**
* ui-plan browser half on a real SlotsService: the plugin occupies the
* conversation-declared `conversation.input.plan` single seat with the plan
* toggle chip; the injected face executes /plan or /plan off by direction and
* folds admission outcomes into null (admitted) or a user-visible failure
* line; teardown empties the seat (HMR safety).
* conversation-declared `conversation.input.plan` single seat with the active
* plan status chip; the injected face executes /plan off and folds admission
* outcomes into null (admitted) or a user-visible failure line; teardown
* empties the seat (HMR safety).
*/
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
@@ -49,7 +49,7 @@ describe('ui-plan browser apply', () => {
.rejects.toThrow(/slot "conversation.input.plan" is not declared/)
})
it('registers the chip, executes /plan by direction, and unregisters on teardown', async () => {
it('registers the chip, executes /plan off, and unregisters on teardown', async () => {
const b = await bench()
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
@@ -57,22 +57,20 @@ describe('ui-plan browser apply', () => {
expect(entry.component).toBe(PlanChip)
const injected = (entry.inject as unknown as (id: SessionId) => PlanChipInjected)(SID)
await expect(injected.setPlanMode(false)).resolves.toBeNull()
await expect(injected.exitPlanMode()).resolves.toBeNull()
expect(b.execute).toHaveBeenLastCalledWith({ sessionId: SID, line: '/plan off' })
await expect(injected.setPlanMode(true)).resolves.toBeNull()
expect(b.execute).toHaveBeenLastCalledWith({ sessionId: SID, line: '/plan' })
// Business failure folds to the composer-visible line.
b.execute.mockResolvedValueOnce({
result: { ok: false as const, error: { code: 'session-not-found', message: 'gone', details: {} } },
} as never)
await expect(injected.setPlanMode(false)).resolves.toBe('gonesession-not-found')
await expect(injected.exitPlanMode()).resolves.toBe('gonesession-not-found')
// Unmatched admission (plan-mode not composed host-side) is also a failure line.
b.execute.mockResolvedValueOnce({
result: { ok: true as const, value: { matched: false as const } },
} as never)
await expect(injected.setPlanMode(true)).resolves.toBe('未知命令:/plan')
await expect(injected.exitPlanMode()).resolves.toBe('未知命令:/plan off')
await fiber.dispose()
expect(b.slots.entries('conversation.input.plan')).toHaveLength(0)

View File

@@ -1,11 +1,9 @@
// @vitest-environment jsdom
/**
* PlanChip over the `plan` projection: nothing renders while the capability
* is absent; with the capability present the chip renders in both states with
* aria-pressed following the effective target (pending folds — /plan shows
* pressed immediately, /plan off unpressed immediately); clicking executes
* the command toward the opposite target and surfaces direction-specific
* failures while the projection still owns the displayed state.
* is absent or the effective target is the default mode; while plan mode is
* the target, the chip executes /plan off and remains visible through failures
* until the projection confirms the exit.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
@@ -18,98 +16,74 @@ afterEach(cleanup)
function setup(
plan: PlanProjection | undefined,
setPlanMode = vi.fn((_on: boolean) => Promise.resolve<string | null>(null)),
exitPlanMode = vi.fn(() => Promise.resolve<string | null>(null)),
locked = false,
) {
const store = createSnapshotStore<{ value: PlanProjection | undefined }>({ value: plan })
const useProjection = (_key: string, selector?: (v: unknown) => unknown) =>
bindSnapshotSelector(store)(s => (selector ?? (v => v))(s.value))
const props = { useProjection, locked, setPlanMode } as unknown as PlanChipProps
const props = { useProjection, locked, exitPlanMode } as unknown as PlanChipProps
const view = render(<PlanChip {...props} />)
return { store, setPlanMode, view }
return { store, exitPlanMode, view }
}
const onChip = () => screen.getByRole('button', { name: 'Plan mode on, press to turn off' })
const offChip = () => screen.getByRole('button', { name: 'Plan mode off, press to turn on' })
const chip = () => screen.getByRole('button', { name: 'Plan mode on, press to turn off' })
describe('PlanChip', () => {
it('renders nothing while the capability is absent', () => {
it('renders nothing for an absent capability or a default-mode target', () => {
const absent = setup(undefined)
expect(absent.view.container.innerHTML).toBe('')
cleanup()
const inactive = setup({ active: false, pending: false })
expect(inactive.view.container.innerHTML).toBe('')
cleanup()
const leaving = setup({ active: true, pending: true })
expect(leaving.view.container.innerHTML).toBe('')
})
it('reflects the effective target as the pressed state, folding pending', () => {
setup({ active: false, pending: false })
expect(offChip().getAttribute('aria-pressed')).toBe('false')
cleanup()
it('renders the Plan status for active and pending-entry targets', () => {
setup({ active: true, pending: false })
expect(onChip().getAttribute('aria-pressed')).toBe('true')
expect(chip().textContent).toBe('Plan')
cleanup()
// /plan just ran (command/run folded, plan/mode not yet): target is plan.
setup({ active: false, pending: true })
expect(onChip().getAttribute('aria-pressed')).toBe('true')
cleanup()
// Active with a pending exit: the target is default — already unpressed.
setup({ active: true, pending: true })
expect(offChip().getAttribute('aria-pressed')).toBe('false')
expect(chip().textContent).toBe('Plan')
})
it('unpressed chip executes /plan (on) once and follows the projection up', async () => {
it('executes /plan off once and follows the projection down', async () => {
let resolve!: (value: string | null) => void
const setPlanMode = vi.fn((_on: boolean) => new Promise<string | null>((done) => { resolve = done }))
const { store } = setup({ active: false, pending: false }, setPlanMode)
fireEvent.click(offChip())
expect(setPlanMode).toHaveBeenCalledTimes(1)
expect(setPlanMode).toHaveBeenLastCalledWith(true)
// Busy while its own call is in flight.
fireEvent.click(offChip())
expect(setPlanMode).toHaveBeenCalledTimes(1)
const exitPlanMode = vi.fn(() => new Promise<string | null>((done) => { resolve = done }))
const { store } = setup({ active: true, pending: false }, exitPlanMode)
fireEvent.click(chip())
expect(exitPlanMode).toHaveBeenCalledTimes(1)
fireEvent.click(chip())
expect(exitPlanMode).toHaveBeenCalledTimes(1)
resolve(null)
// The command's run record folds: target flips, the chip presses.
store.set({ value: { active: false, pending: true } })
await waitFor(() => {
expect(onChip().getAttribute('aria-pressed')).toBe('true')
})
})
it('pressed chip executes /plan off and follows the projection down', async () => {
const setPlanMode = vi.fn((_on: boolean) => Promise.resolve<string | null>(null))
const { store } = setup({ active: true, pending: false }, setPlanMode)
fireEvent.click(onChip())
expect(setPlanMode).toHaveBeenLastCalledWith(false)
store.set({ value: { active: true, pending: true } })
await waitFor(() => {
expect(offChip().getAttribute('aria-pressed')).toBe('false')
expect(screen.queryByRole('button', { name: 'Plan mode on, press to turn off' })).toBeNull()
})
})
it('disables under the locked owner prop', () => {
setup({ active: true, pending: false }, vi.fn(), true)
expect((onChip() as HTMLButtonElement).disabled).toBe(true)
expect((chip() as HTMLButtonElement).disabled).toBe(true)
})
it('surfaces direction-specific admission and transport failures while staying visible', async () => {
const exitFailing = vi.fn()
it('surfaces admission and transport failures while staying visible', async () => {
const exitPlanMode = vi.fn()
.mockResolvedValueOnce('host said no')
.mockRejectedValueOnce(new Error('network down'))
.mockRejectedValueOnce('socket closed')
setup({ active: true, pending: false }, exitFailing)
fireEvent.click(onChip())
setup({ active: true, pending: false }, exitPlanMode)
fireEvent.click(chip())
expect((await screen.findByText('退出 plan mode 失败')).getAttribute('title')).toBe('host said no')
expect(onChip()).toBeTruthy()
expect(chip()).toBeTruthy()
fireEvent.click(onChip())
fireEvent.click(chip())
expect(await screen.findByTitle('network down')).toBeTruthy()
fireEvent.click(onChip())
fireEvent.click(chip())
expect(await screen.findByTitle('socket closed')).toBeTruthy()
cleanup()
const enterFailing = vi.fn().mockResolvedValueOnce('agent busy')
setup({ active: false, pending: false }, enterFailing)
fireEvent.click(offChip())
expect((await screen.findByText('进入 plan mode 失败')).getAttribute('title')).toBe('agent busy')
expect(offChip()).toBeTruthy()
})
it('ignores in-flight fulfillment and rejection after unmount', () => {
@@ -118,14 +92,14 @@ describe('PlanChip', () => {
{ active: true, pending: false },
vi.fn(() => new Promise<string | null>((done) => { resolve = done })),
)
fireEvent.click(onChip())
fireEvent.click(chip())
successful.view.unmount()
expect(() => { resolve(null) }).not.toThrow()
let reject!: (reason: unknown) => void
const setPlanMode = vi.fn(() => new Promise<string | null>((_done, fail) => { reject = fail }))
const { view } = setup({ active: true, pending: false }, setPlanMode)
fireEvent.click(onChip())
const exitPlanMode = vi.fn(() => new Promise<string | null>((_done, fail) => { reject = fail }))
const { view } = setup({ active: true, pending: false }, exitPlanMode)
fireEvent.click(chip())
view.unmount()
expect(() => { reject(new Error('late')) }).not.toThrow()
})

View File

@@ -20,6 +20,9 @@
{
"path": "../ui-conversation"
},
{
"path": "../ui-primitives"
},
{
"path": "../ui-slots"
},