Просмотр исходного кода

feat(web): open submitted plans automatically with artifact cards

Yichen Jiang 1 неделя назад
Родитель
Сommit
1e1dd43fee

+ 2 - 2
.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.i18n.yaml

@@ -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 .agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md
-2026-09-17-persistent-plan-cards.md: e0a630c8395e4bf3d8d93bef010afffd177488d7
-2026-09-17-persistent-plan-cards.zh.md: c5722cd065260c5db6fd21a664b23545ca14c79b
+2026-09-17-persistent-plan-cards.md: 5b790123bf10800b94ed28595f4127031df8bd04
+2026-09-17-persistent-plan-cards.zh.md: 155280d94f250ee2725c92f1f601331259e68728

+ 1 - 1
.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md

@@ -14,7 +14,7 @@ A plan review occupies the composer only until the user answers or dismisses it.
 
 The review intent carries the tool-call identity. The question plugin declares an action slot, and the plan plugin contributes an opener there and on the historical card. Both open the resource identified by Session and invocation. Sidebar layout retains that address; the resource provider reads the existing Session history, paging backwards when the invocation is older than the opening window. Its temporary follow closes after the opening snapshot.
 
-The document is read-only. The pending review shows its status, sidebar opener, and decision buttons; the complete text is read in the sidebar. Approval stays in the pending-question owner; reopening a historical document does not revive a settled review or authorize implementation. Different submissions remain distinct even when they share a heading.
+The document is read-only. The pending review shows its status, sidebar opener, and decision buttons; the complete text opens automatically in the sidebar. A transient Session-scoped store records each automatically opened invocation, so review remounts preserve manual closure. Historical cards never trigger automatic navigation. The cards use the file-delivery treatment with a Markdown icon, plan title, and Open action. Approval stays in the pending-question owner; reopening a historical document does not revive a settled review or authorize implementation. Different submissions remain distinct even when they share a heading.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 审批意图携带工具调用标识。提问插件声明操作插槽,计划插件在该插槽和历史卡片中提供打开入口。两者都打开由会话和调用标识确定的资源。侧边栏布局保留该地址;资源 provider 读取已有会话历史,当调用早于初始窗口时向前分页。临时 follow 在取得初始快照后关闭。
 
-文档只读。待处理审批显示状态、侧边栏入口和决定按钮;完整正文在侧边栏阅读。审批仍由待处理提问的属主负责;重新打开历史文档不会恢复已结束的审批,也不会授权实施。即使标题相同,不同提交仍彼此独立。
+文档只读。待处理审批显示状态、侧边栏入口和决定按钮;完整正文自动在侧边栏打开。临时的会话级 store 记录每个已自动打开的调用,让审批组件重新挂载时仍保留用户的关闭选择。历史卡片不会触发自动导航。卡片采用文件产物样式,包含 Markdown 图标、计划标题和“打开”操作。审批仍由待处理提问的属主负责;重新打开历史文档不会恢复已结束的审批,也不会授权实施。即使标题相同,不同提交仍彼此独立。
 
 ## Alternatives considered
 

+ 8 - 1
apps/web/tests/plan-review.e2e.ts

@@ -101,9 +101,9 @@ describe('web e2e: plan review takeover round trip', () => {
 
     const planCard = page.locator('[data-plan-card]')
     await planCard.waitFor({ state: 'visible' })
-    await card.getByRole('button', { name: 'Open plan in sidebar' }).click()
     const preview = page.locator('[data-plan-preview]')
     await preview.waitFor({ state: 'visible' })
+    expect(await card.getByRole('button', { name: 'Approve' }).isVisible()).toBe(true)
     expect(await preview.getByRole('heading', { level: 1 }).textContent()).toContain('--greeting')
     await card.getByRole('button', { name: 'Open plan in sidebar' }).click()
     expect(await page.locator('[data-plan-preview]').count()).toBe(1)
@@ -111,6 +111,7 @@ describe('web e2e: plan review takeover round trip', () => {
       await compareOrRefreshGolden(PREVIEW_EXPECTED, await captureStableAria(page, '[data-plan-preview]', scaffold.workspaceCwd), MODE)
     }
     await page.locator('[data-sidebar-right-toggle]').click()
+    await preview.waitFor({ state: 'hidden' })
     await planCard.click()
     await preview.waitFor({ state: 'visible' })
 
@@ -182,6 +183,8 @@ describe('web e2e: dismissed plan history', () => {
       await input.press('Enter')
       const review = page.locator('[data-plan-review-key]')
       await review.waitFor({ state: 'visible' })
+      await page.locator('[data-plan-preview]').waitFor({ state: 'visible' })
+      await page.locator('[data-sidebar-right-toggle]').click()
       await review.getByRole('button', { name: 'Chat about it', exact: true }).click()
       await settled
       const results = events.filter(event => event.type === 'tool/result')
@@ -197,6 +200,10 @@ describe('web e2e: dismissed plan history', () => {
       await page.locator('[data-plan-card]').waitFor({ state: 'visible' })
       await page.locator('[data-plan-preview]').waitFor({ state: 'visible' })
       expect(await page.locator('[data-plan-preview]').getByRole('heading', { level: 1 }).textContent()).toContain('--greeting')
+      await page.locator('[data-sidebar-right-toggle]').click()
+      await page.reload({ waitUntil: 'load' })
+      await page.locator('[data-plan-card]').waitFor({ state: 'visible' })
+      expect(await page.locator('[data-plan-preview]').count()).toBe(0)
       expect(tripwire.pageErrors).toEqual([])
     } finally {
       await browser?.close()

+ 2 - 2
packages/client/ui-plan/README.i18n.yaml

@@ -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: 09c112fe395045a5fa9302efef73ee6a44eebb0c
-README.zh.md: cbb58febadeb6bda92b0d3e23109c4d9ed7a108e
+README.md: 7f68df2d44e6e540a559e4769b7550fc52e5cfae
+README.zh.md: ac9977aa72787b73fb84f46a0e9f0f9515e72ad5

+ 2 - 2
packages/client/ui-plan/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Plan mode lets you review a plan before implementation. Enter with `/plan`, leave with the composer chip, and open a submitted plan in the right sidebar from its review card or permanent Chat card. Plans remain available after approval, rejection, or dismissal; reopening a plan focuses its existing tab, and browser reload restores the document from Session history.
+Plan mode lets you review a plan before implementation. Enter with `/plan` and leave with the composer chip. Submitted plans open automatically in the right sidebar for review and remain available from their permanent Chat cards after approval, rejection, or dismissal. Reopening a plan focuses its existing tab, and browser reload restores the document from Session history.
 
 ## Table of Contents
 
@@ -33,7 +33,7 @@ While the effective target is plan mode, the seat renders the warn-colored "Plan
 
 ### Reading submitted plans
 
-Each submitted plan has a permanent Chat card outside the Turn process disclosure. Click its title, or the review strip’s Open plan in sidebar button, to read and copy the complete Markdown. Different submissions retain separate tabs. Closing the document or review does not delete the plan; the review buttons alone decide whether implementation may begin.
+Each submitted plan has a permanent Chat card outside the Turn process disclosure, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s Open plan in sidebar button to read and copy the complete Markdown. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin.
 
 ### Failures
 

+ 2 - 2
packages/client/ui-plan/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-计划模式让你在实施前审阅计划。通过 `/plan` 进入,通过编辑器中的状态按钮退出,并从审批卡片或聊天历史中的常驻计划卡片在右侧边栏打开全文。批准、拒绝或关闭审批后,计划仍可查看;重新打开同一计划会聚焦已有标签页,刷新浏览器后会从会话历史恢复正文。
+计划模式让你在实施前审阅计划。通过 `/plan` 进入,通过编辑器中的状态按钮退出。提交的计划自动在右侧边栏打开供审阅;批准、拒绝或关闭审批后,仍可通过聊天历史中的常驻卡片查看。重新打开同一计划会聚焦已有标签页,刷新浏览器后会从会话历史恢复正文。
 
 ## 目录
 
@@ -33,7 +33,7 @@ kind: "package-reference"
 
 ### 查看已提交的计划
 
-每次提交的计划都有一张常驻聊天卡片,不随回合运行过程折叠。点击卡片标题,或审批条带上的“在侧边栏打开计划”按钮,即可阅读和复制完整 Markdown。不同提交保留独立标签页。关闭文档或审批卡片不会删除计划;是否开始实施仍由审批按钮决定。
+每次提交的计划都有一张常驻聊天卡片,不随回合运行过程折叠,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“在侧边栏打开计划”按钮可阅读和复制完整 Markdown。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。
 
 ### 失败
 

+ 1 - 0
packages/client/ui-plan/package.json

@@ -48,6 +48,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",

+ 19 - 8
packages/client/ui-plan/src/client/PlanCard.tsx

@@ -1,9 +1,11 @@
 /** Persistent transcript card and pending-review action use the same plan resource opener. */
-import { IconFullscreenOutline16, IconPlanOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import { useEffect } from 'react'
+import { FileTypeIcon, IconFullscreenOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type {} from '@deepseek-ai/dsh-client-ui-chat/client'
-import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type { InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
 import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
 import type {} from '@deepseek-ai/dsh-client-ui-user-questions/client'
+import type { createPlanReviewStore } from './review-store.ts'
 import css from './PlanPreview.module.css'
 
 /** Session-bound navigation injected into both plan entry points. */
@@ -22,20 +24,29 @@ export function PlanCard({ node, openPlan, t }: PropsRuntime<'conversation.chat.
     <button type="button" className={css.card} data-plan-card={node.data.callId}
       aria-label={t('preview.openNamed', { title: node.data.title })}
       onClick={() => { openPlan(node.data.callId) }}>
-      <IconPlanOutline14 size={18} />
-      <span className={css.cardTitle}>{node.data.title}</span>
-      <IconFullscreenOutline16 size={16} />
+      <span className={css.cardIcon}><FileTypeIcon kind="markdown" size={20} /></span>
+      <span className={css.cardDetails}>
+        <span className={css.cardTitle}>{node.data.title}</span>
+        <span className={css.cardDescription}>{t('preview.document')}</span>
+      </span>
+      <span className={css.cardOpen}>{t('preview.action')}</span>
     </button>
   )
 }
 
 /**
- * Add a sidebar opener to the pending review without changing its answer.
- * @param props - Review identity, localized copy, and navigation.
+ * Open each pending plan automatically and retain a manual opener without answering it.
+ * @param props - Review identity, Session store, localized copy, and navigation.
  * @returns an opener when the asker supplied a logged invocation identity.
  */
-export function PlanReviewOpen({ review, openPlan, t }: PropsRuntime<'conversation.plan-review.actions'> & InjectFace<PlanOpenInjected> & PropsLocale<'plan'>) {
+export function PlanReviewOpen({ review, openPlan, t, useStore, actions }: PropsRuntime<'conversation.plan-review.actions'> & InjectFace<PlanOpenInjected> & PropsLocale<'plan'> & PropsStore<ReturnType<typeof createPlanReviewStore>>) {
   const callId = review.callId
+  const opened = useStore(state => callId !== undefined && state.opened[callId] === true)
+  useEffect(() => {
+    if (callId === undefined || opened) return
+    actions.markOpened(callId)
+    openPlan(callId)
+  }, [callId, opened, openPlan, actions])
   if (callId === undefined) return null
   return <button type="button" className={css.reviewOpen} title={t('preview.open')} aria-label={t('preview.open')}
     onClick={() => { openPlan(callId) }}><IconFullscreenOutline16 size={16} /></button>

+ 18 - 8
packages/client/ui-plan/src/client/PlanPreview.module.css

@@ -1,22 +1,32 @@
 .card {
+  --plan-card-fill: var(--dsw-static-neutral-50);
+  --plan-card-hover: var(--dsw-static-neutral-100);
+  box-sizing: border-box;
   display: flex;
   align-items: center;
-  gap: 12px;
+  gap: 10px;
+  min-width: 0;
   width: 100%;
-  padding: 16px 18px;
+  height: 60px;
+  padding: 8px 10px;
   margin: 12px 0;
   border: 0.5px solid var(--dsw-alias-border-l1);
-  border-radius: 14px;
-  background: transparent;
-  color: var(--dsw-alias-label-secondary);
+  border-radius: 18px;
+  background: var(--plan-card-fill);
+  color: var(--dsw-alias-label-primary);
   font: inherit;
   text-align: left;
   cursor: pointer;
+  transition: background-color 120ms ease;
 }
-.card:hover { background: var(--dsw-alias-interactive-bg-hover); }
+:global(body[data-ds-dark-theme]) .card { --plan-card-fill: var(--dsw-static-neutral-850); --plan-card-hover: var(--dsw-static-neutral-800); }
+.card:hover { background: var(--plan-card-hover); }
 .card:focus-visible, .reviewOpen:focus-visible { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 3px; }
-.card > svg { flex-shrink: 0; }
-.cardTitle { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
+.cardIcon { box-sizing: border-box; flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--plan-card-fill); }
+.cardDetails { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; }
+.cardTitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; line-height: 20px; }
+.cardDescription { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 16px; }
+.cardOpen { box-sizing: border-box; display: inline-flex; flex: none; align-items: center; height: 28px; padding: 4px 8px; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: 10px; background: var(--dsw-alias-button-floating-fill); font-size: 12px; line-height: 18px; }
 .reviewOpen { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; }
 .reviewOpen:hover { background: var(--dsw-alias-interactive-bg-hover); }
 .preview { position: relative; height: 100%; overflow: auto; padding: 20px 24px 40px; box-sizing: border-box; }

+ 3 - 1
packages/client/ui-plan/src/client/index.ts

@@ -20,6 +20,7 @@ import { PlanPreview, PlanTitle } from './PlanPreview.tsx'
 import { planDefinition } from './plan-definition.ts'
 import { planResourceProvider } from './plan-resource.ts'
 import { planAddress, parsePlanAddress } from './plan.ts'
+import { createPlanReviewStore } from './review-store.ts'
 import { PlanChip } from './PlanModeControl.tsx'
 import { en, zh, type PlanKey } from './locales.ts'
 
@@ -66,11 +67,12 @@ export function apply(ctx: ClientContext): void {
   const open = (sessionId: SessionId): PlanOpenInjected => ({
     openPlan: (callId) => { ctx.sidebarRight.openResourceIn(sessionId, planAddress({ sessionId, callId })) },
   })
+  const reviewStore = createPlanReviewStore()
   ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
     name: 'conversation.chat.node', key: 'submitted-plan', locale: NS, inject: open,
   }, PlanCard))
   ctx.slots.inject('conversation.plan-review.actions', () => ctx.slots.register({
-    name: 'conversation.plan-review.actions', id: previewId, locale: NS, inject: open,
+    name: 'conversation.plan-review.actions', id: previewId, locale: NS, inject: open, store: reviewStore,
   }, PlanReviewOpen))
   ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
     name: 'sidebar.right.pane.tab', key: previewId, locale: NS,

+ 4 - 0
packages/client/ui-plan/src/client/locales.ts

@@ -4,6 +4,8 @@
 export const zh = {
   'chip.label': 'Plan',
   'preview.title': '计划',
+  'preview.document': '计划 · Markdown',
+  'preview.action': '打开',
   'preview.open': '在侧边栏打开计划',
   'preview.openNamed': '打开计划:{title}',
   'preview.loading': '正在读取计划…',
@@ -22,6 +24,8 @@ export type PlanKey = keyof typeof zh
 export const en = {
   'chip.label': 'Plan',
   'preview.title': 'Plan',
+  'preview.document': 'Plan · Markdown',
+  'preview.action': 'Open',
   'preview.open': 'Open plan in sidebar',
   'preview.openNamed': 'Open plan: {title}',
   'preview.loading': 'Loading plan…',

+ 24 - 0
packages/client/ui-plan/src/client/review-store.ts

@@ -0,0 +1,24 @@
+/** Session-owned memory of pending plans already opened automatically. */
+import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-store'
+import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
+
+interface PlanReviewState {
+  opened: Partial<Record<ToolCallId, true>>
+}
+
+type PlanReviewActions = {
+  markOpened: (draft: PlanReviewState, callId: ToolCallId) => void
+}
+
+/**
+ * Keep manual sidebar closure effective across review component remounts.
+ * @returns a transient store handle whose instances belong to Session scopes.
+ */
+export function createPlanReviewStore(): EngineStoreHandle<PlanReviewState, PlanReviewActions> {
+  return defineStore({
+    init: (): PlanReviewState => ({ opened: {} }),
+    actions: {
+      markOpened: (draft, callId) => { draft.opened[callId] = true },
+    },
+  })
+}

+ 37 - 3
packages/client/ui-plan/tests/plan-preview.client.spec.tsx

@@ -9,6 +9,7 @@ import { PlanCard, PlanReviewOpen } from '../src/client/PlanCard.tsx'
 import { PlanPreview, PlanTitle } from '../src/client/PlanPreview.tsx'
 import { planAddress, parsePlanAddress, submittedPlan } from '../src/client/plan.ts'
 import { planDefinition } from '../src/client/plan-definition.ts'
+import { createPlanReviewStore } from '../src/client/review-store.ts'
 
 afterEach(cleanup)
 const markdown = '# Keep this plan\n\n## Goal\n\n- Review\n- Implement'
@@ -68,6 +69,9 @@ describe('plan entry points and document', () => {
       const openPlan = vi.fn()
       const props = { node: { data: plan }, t: makeTranslate(dictionary, commonEn), openPlan } as unknown as Parameters<typeof PlanCard>[0]
       const view = render(<PlanCard {...props} />)
+      expect(openPlan).not.toHaveBeenCalled()
+      expect(screen.getByText(dictionary['preview.action'])).toBeTruthy()
+      expect(screen.queryByText('Implement')).toBeNull()
       fireEvent.click(screen.getByRole('button'))
       expect(openPlan).toHaveBeenCalledWith(plan.callId)
       expect(screen.getByText(plan.title)).toBeTruthy()
@@ -76,13 +80,43 @@ describe('plan entry points and document', () => {
   })
   it('opens the review without approving or cancelling it', () => {
     const openPlan = vi.fn()
-    const props = { review: { callId: plan.callId }, t, openPlan } as unknown as Parameters<typeof PlanReviewOpen>[0]
+    const store = createPlanReviewStore().create()
+    const stateProps = {
+      actions: store.actions,
+      useStore: (select: (state: ReturnType<typeof store.getSnapshot>) => unknown) => select(store.getSnapshot()),
+    }
+    const props = { review: { callId: plan.callId }, t, openPlan, ...stateProps } as unknown as Parameters<typeof PlanReviewOpen>[0]
     const view = render(<PlanReviewOpen {...props} />)
+    expect(openPlan).toHaveBeenCalledExactlyOnceWith(plan.callId)
+    view.rerender(<PlanReviewOpen {...props} />)
+    expect(openPlan).toHaveBeenCalledTimes(1)
     fireEvent.click(screen.getByRole('button', { name: 'Open plan in sidebar' }))
-    expect(openPlan).toHaveBeenCalledWith(plan.callId)
-    view.rerender(<PlanReviewOpen {...{ review: {}, t, openPlan } as unknown as Parameters<typeof PlanReviewOpen>[0]} />)
+    expect(openPlan).toHaveBeenCalledTimes(2)
+    view.rerender(<PlanReviewOpen {...{ ...props, review: {} } as unknown as Parameters<typeof PlanReviewOpen>[0]} />)
     expect(screen.queryByRole('button')).toBeNull()
   })
+  it('preserves manual closure across remounts and opens a new submission', () => {
+    const store = createPlanReviewStore().create()
+    const openPlan = vi.fn()
+    const props = { review: { callId: plan.callId }, t, openPlan, actions: store.actions,
+      useStore: (select: (state: ReturnType<typeof store.getSnapshot>) => unknown) => select(store.getSnapshot()),
+    } as unknown as Parameters<typeof PlanReviewOpen>[0]
+    const first = render(<PlanReviewOpen {...props} />)
+    first.unmount()
+    const second = render(<PlanReviewOpen {...props} />)
+    expect(openPlan).toHaveBeenCalledTimes(1)
+    const revised = submittedPlan({ ...call, data: { ...call.data, callId: 'call:2' } })!
+    const nextProps = { ...props, review: { callId: revised.callId } } as unknown as Parameters<typeof PlanReviewOpen>[0]
+    second.rerender(<PlanReviewOpen {...nextProps} />)
+    expect(openPlan).toHaveBeenLastCalledWith(revised.callId)
+    expect(openPlan).toHaveBeenCalledTimes(2)
+    second.unmount()
+    const other = createPlanReviewStore().create()
+    render(<PlanReviewOpen {...{ ...props, actions: other.actions,
+      useStore: (select: (state: ReturnType<typeof other.getSnapshot>) => unknown) => select(other.getSnapshot()),
+    } as unknown as Parameters<typeof PlanReviewOpen>[0]} />)
+    expect(openPlan).toHaveBeenCalledTimes(3)
+  })
   it('shows restored Markdown and its heading as the tab title', () => {
     const props = {
       t, useTabInfo: () => ({ tab: { title: 'Plan', navigation: { address: planAddress(target) } } }),

+ 3 - 0
packages/client/ui-plan/tsconfig.json

@@ -8,6 +8,9 @@
     "src"
   ],
   "references": [
+    {
+      "path": "../store"
+    },
     {
       "path": "../../api/remotes/tsconfig.client.json"
     },

+ 3 - 0
pnpm-lock.yaml

@@ -3623,6 +3623,9 @@ importers:
       '@deepseek-ai/dsh-client-resources':
         specifier: workspace:^
         version: link:../resources
+      '@deepseek-ai/dsh-client-store':
+        specifier: workspace:^
+        version: link:../store
       '@deepseek-ai/dsh-client-test-runtime':
         specifier: workspace:^
         version: link:../../test-support/client-runtime