diff --git a/apps/cli/cordis.yml b/apps/cli/cordis.yml index eb04848269..add18c7b8e 100644 --- a/apps/cli/cordis.yml +++ b/apps/cli/cordis.yml @@ -78,14 +78,33 @@ config: agents: [] -# The native DeepSeek adapter; reads the key/base-url the boot's layered -# .env loading (cwd then $DSH_HOME) left in the environment. +# User-settings document (`$DSH_HOME/settings.yaml`, hot-reloaded): the web +# settings page writes it through `settings.update`/`settings.replace`, and an +# external edit converges every open surface through `host/settings-changed`. +- id: settings + name: '@deepseek-ai/dsh-settings-local' + +# Credential store: the live process environment over `$DSH_HOME/.env` +# (owner-only file, hot-reloaded). The web page's key inputs write it through +# `credentials.set`; adapters resolve references per request. +- id: credentials + name: '@deepseek-ai/dsh-credentials-local' + +# The native DeepSeek adapter; the API key resolves per request through the +# credential store above (default reference DEEPSEEK_API_KEY), so no key is +# inlined here and a missing one fails the request, not the boot. - id: llm-deepseek name: '@deepseek-ai/dsh-llm-deepseek' config: - apiKey: !!js process.env.DEEPSEEK_API_KEY baseURL: !!js process.env.DEEPSEEK_BASE_URL +# The pi-ai multi-provider twin, mounted dormant: zero routes until the +# `llm-pi-ai:` settings section supplies provider profiles — exactly what the +# web Models page writes. Configured routes register live and drop when the +# section empties. +- id: llm-pi-ai + name: '@deepseek-ai/dsh-llm-pi-ai' + # Transient-failure recovery around the loop's model calls (same policy as # the TUI's agent-spine composition; defaults: 2 retries, 500ms→10s backoff). - id: llm-retry diff --git a/apps/cli/package.json b/apps/cli/package.json index e31e7378a0..2aefe661f5 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -47,6 +47,7 @@ "@deepseek-ai/dsh-command-goal": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-compact-basic": "workspace:^", + "@deepseek-ai/dsh-credentials-local": "workspace:^", "@deepseek-ai/dsh-frontend": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-fs-policy": "workspace:^", @@ -56,6 +57,7 @@ "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", + "@deepseek-ai/dsh-llm-pi-ai": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-plan-mode": "workspace:^", @@ -65,6 +67,7 @@ "@deepseek-ai/dsh-session-projection-cache": "workspace:^", "@deepseek-ai/dsh-session-title": "workspace:^", "@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^", + "@deepseek-ai/dsh-settings-local": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-skill-local": "workspace:^", "@deepseek-ai/dsh-spill-local": "workspace:^", diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts new file mode 100644 index 0000000000..d32bf21afc --- /dev/null +++ b/apps/web/tests/models-settings.e2e.ts @@ -0,0 +1,116 @@ +// Web e2e scenario: the Models settings page end to end through the real +// wire — the dormant pi-ai directory renders as the add vocabulary, adding a +// provider writes the settings document and registers the route live (the +// row's 已启用 badge is the topology invalidation landing), and the key input +// stores a credential write-only into the harness home's .env. Zero model +// calls: configuration is pure settings/credentials/llm-domain traffic, so +// there is no fixture and a stray stream would fail loud on the open seam. +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import.meta.url)) +const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md') +const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md') +const MODE = webSnapshotMode() + +describe('web e2e: Models settings page configures a dormant provider', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + browser = await chromium.launch() + page = await browser.newPage({ viewport: { width: 1680, height: 1000 } }) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('renders the dormant directory as the add vocabulary', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-models-empty')) + await page.getByRole('button', { name: '设置', exact: true }).click() + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '模型' }).click() + await dialog.getByText('填入各提供方的 API 密钥即可使用其模型。').waitFor({ timeout: 10_000 }) + // The dormant pi-ai adapter contributes its whole installed catalog; no + // provider is configured yet, so the page is one add-select. + const add = dialog.getByLabel('添加提供方') + await add.waitFor({ timeout: 10_000 }) + // The select renders before the directory join settles; poll until the + // dormant catalog landed. + await expect.poll(async () => add.locator('option').count(), { timeout: 10_000 }).toBeGreaterThan(30) + const options = await add.locator('option').allTextContents() + expect(options).toContain('anthropic') + expect(options).toContain('openai') + const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(EMPTY_EXPECTED, snapshot, MODE) + }, 60_000) + + it('adds a provider through the schema-driven editor and the route registers live', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-models-add')) + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByLabel('添加提供方').selectOption('anthropic') + // The editor is the real pi-ai profile schema rendered field by field; + // the credential-reference control is the role-tagged override. + const ref = dialog.getByLabel('API 密钥环境变量') + await ref.waitFor({ timeout: 10_000 }) + // A test-owned reference name keeps this hermetic: a developer's real + // ANTHROPIC_API_KEY in the process environment must not flip the badge. + await ref.fill('E2E_ANTHROPIC_KEY') + await dialog.getByRole('button', { name: '保存', exact: true }).click() + // The write lands in settings.yaml, the dormant route registers, the + // topology frame invalidates the page, and the reloaded join shows the + // row live with its credential still missing. + const row = dialog.getByText('anthropic', { exact: true }).first() + await row.waitFor({ timeout: 10_000 }) + await dialog.getByText('已启用').waitFor({ timeout: 10_000 }) + await dialog.getByText('缺少密钥').waitFor({ timeout: 10_000 }) + const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') + expect(document).toContain('llm-pi-ai:') + expect(document).toContain('anthropic:') + expect(document).toContain('apiKeyEnv: E2E_ANTHROPIC_KEY') + }, 60_000) + + it('stores the API key write-only and the badge flips configured', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-models-key')) + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByRole('button', { name: '编辑' }).click() + const key = dialog.getByLabel('API 密钥', { exact: true }) + await key.waitFor({ timeout: 10_000 }) + await key.fill('sk-ant-e2e-test') + await dialog.getByRole('button', { name: '保存密钥' }).click() + await dialog.getByText('已配置', { exact: true }).waitFor({ timeout: 10_000 }) + // The value went to the harness home's .env — and nowhere in the DOM. + const stored = await readFile(join(scaffold.harnessHome, '.env'), 'utf8') + expect(stored).toContain('E2E_ANTHROPIC_KEY=sk-ant-e2e-test') + expect(await page.content()).not.toContain('sk-ant-e2e-test') + await dialog.getByRole('button', { name: '取消' }).click() + // The row badge converges from the credentials invalidation. + await expect.poll(async () => dialog.getByText('缺少密钥').count(), { timeout: 10_000 }).toBe(0) + const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(CONFIGURED_EXPECTED, snapshot, MODE) + await page.keyboard.press('Escape') + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + + it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { + await assertFixtureInventory(SNAPSHOT_DIR, ['configured.expected.md', 'empty.expected.md']) + }) +}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index ffa159c1dd..90e98ddf57 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -86,6 +86,8 @@ export interface WebScaffold { workspaceCwd: string /** Temp persistence root (seeded sessions land here through the real API). */ persistenceRoot: string + /** Isolated harness home the settings/credentials rows write ($DSH_HOME double). */ + harnessHome: string /** Await a settled turn end: in-process turn/end, then the agent's idle flip (which follows the persistence flush). */ whenTurnSettled(timeoutMs?: number): Promise /** Tear everything down; asserts the replay fixture was fully consumed first (replay/refresh). */ @@ -150,6 +152,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise { // Golden of the freshly opened dialog (default zh, General active). const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) await compareOrRefreshGolden(DIALOG_EXPECTED, snapshot, MODE) - // Section switch: aria-current moves; Models is deliberately empty. + // Section switch: aria-current moves (the Models page itself has its own scenario file). await dialog.getByRole('button', { name: '模型' }).click() await expect.poll(() => dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current'), { timeout: 5_000 }).toBe('true') expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBeNull() diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md new file mode 100644 index 0000000000..6aa642a428 --- /dev/null +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -0,0 +1,57 @@ +- dialog "设置": + - navigation: + - text: 设置 + - button "通用设置": + - img + - text: 通用设置 + - button "模型": + - img + - text: 模型 + - button "关闭": + - img + - text: 关闭 + - heading "模型" [level=2] + - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - list: + - listitem: + - text: anthropic 已启用 + - button "编辑" + - button "删除" + - combobox "添加提供方": + - option "+ 添加提供方" [selected] + - option "amazon-bedrock" + - option "ant-ling" + - option "azure-openai-responses" + - option "cerebras" + - option "cloudflare-ai-gateway" + - option "cloudflare-workers-ai" + - option "deepseek" + - option "fireworks" + - option "github-copilot" + - option "google" + - option "google-vertex" + - option "groq" + - option "huggingface" + - option "kimi-coding" + - option "minimax" + - option "minimax-cn" + - option "mistral" + - option "moonshotai" + - option "moonshotai-cn" + - option "nvidia" + - option "openai" + - option "openai-codex" + - option "opencode" + - option "opencode-go" + - option "openrouter" + - option "qwen-token-plan" + - option "qwen-token-plan-cn" + - option "together" + - option "vercel-ai-gateway" + - option "xai" + - option "xiaomi" + - option "xiaomi-token-plan-ams" + - option "xiaomi-token-plan-cn" + - option "xiaomi-token-plan-sgp" + - option "zai" + - option "zai-coding-cn" diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md new file mode 100644 index 0000000000..da66c40743 --- /dev/null +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -0,0 +1,54 @@ +- dialog "设置": + - navigation: + - text: 设置 + - button "通用设置": + - img + - text: 通用设置 + - button "模型": + - img + - text: 模型 + - button "关闭": + - img + - text: 关闭 + - heading "模型" [level=2] + - paragraph: 填入各提供方的 API 密钥即可使用其模型。 + - list + - combobox "添加提供方": + - option "+ 添加提供方" [selected] + - option "amazon-bedrock" + - option "ant-ling" + - option "anthropic" + - option "azure-openai-responses" + - option "cerebras" + - option "cloudflare-ai-gateway" + - option "cloudflare-workers-ai" + - option "deepseek" + - option "fireworks" + - option "github-copilot" + - option "google" + - option "google-vertex" + - option "groq" + - option "huggingface" + - option "kimi-coding" + - option "minimax" + - option "minimax-cn" + - option "mistral" + - option "moonshotai" + - option "moonshotai-cn" + - option "nvidia" + - option "openai" + - option "openai-codex" + - option "opencode" + - option "opencode-go" + - option "openrouter" + - option "qwen-token-plan" + - option "qwen-token-plan-cn" + - option "together" + - option "vercel-ai-gateway" + - option "xai" + - option "xiaomi" + - option "xiaomi-token-plan-ams" + - option "xiaomi-token-plan-cn" + - option "xiaomi-token-plan-sgp" + - option "zai" + - option "zai-coding-cn" diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 1b0f807d5f..59b544fc89 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -29,6 +29,7 @@ "tests/navigation-panes.e2e.ts", "tests/lifecycle-chrome.e2e.ts", "tests/settings-chrome.e2e.ts", + "tests/models-settings.e2e.ts", "tests/workspace-management.e2e.ts", "tests/replay-round-trip.e2e.ts", "tests/seeded-history.e2e.ts", diff --git a/packages/client/schema-form/tsdown.config.ts b/packages/client/schema-form/tsdown.config.ts new file mode 100644 index 0000000000..c6d22ad7e6 --- /dev/null +++ b/packages/client/schema-form/tsdown.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'tsdown' + +/** + * schema-form is browser-only, but its lib bundle is imported under plain + * Node through consumer lib chains (same posture as ui-primitives). CSS + * imports are stubbed to empty modules: the hashed class maps only matter in + * bundler contexts, which compile src directly and never read lib. + */ +export default defineConfig({ + entry: ['lib/types/index.js', 'lib/types/invariant.js'], + outDir: 'lib', + format: ['esm'], + platform: 'neutral', + target: 'es2024', + fixedExtension: false, + dts: false, + clean: false, + plugins: [{ + name: 'dsh-css-stub', + resolveId(source: string) { + if (!source.endsWith('.css')) return null + return `\0dsh-css-stub:${source}.mjs` + }, + load(id: string) { + if (!id.startsWith('\0dsh-css-stub:')) return null + return 'export default {};' + }, + }], +}) diff --git a/packages/client/ui-models/src/client/ModelsSection.tsx b/packages/client/ui-models/src/client/ModelsSection.tsx index d9b9580de9..7a08441ea9 100644 --- a/packages/client/ui-models/src/client/ModelsSection.tsx +++ b/packages/client/ui-models/src/client/ModelsSection.tsx @@ -28,10 +28,11 @@ export interface ModelsSectionInjected { t: (key: keyof typeof en) => string } -/** Props delivered by the slot outlet. */ -export interface ModelsSectionProps { - injected?: ModelsSectionInjected -} +/** + * Props delivered by the slot outlet: the inject face spread flat (the + * renderer erases the share boundary at the render call). + */ +export type ModelsSectionProps = Partial /** The editor target: an existing row or a dormant directory entry. */ interface EditorTarget { @@ -80,9 +81,9 @@ function StatusBadges({ row, t }: { row: ProviderRow; t: ModelsSectionInjected[' * @returns the section, or null while the shell has not injected yet. */ export function ModelsSection(props: ModelsSectionProps): ReactNode { - const injected = props.injected - if (injected === undefined) return null - return + const { controller, useSnapshot, api, t } = props + if (controller === undefined || useSnapshot === undefined || api === undefined || t === undefined) return null + return } function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode { diff --git a/packages/client/ui-models/tests/components.spec.tsx b/packages/client/ui-models/tests/components.spec.tsx index b501532773..32d4eb73b6 100644 --- a/packages/client/ui-models/tests/components.spec.tsx +++ b/packages/client/ui-models/tests/components.spec.tsx @@ -104,9 +104,11 @@ function scriptedFace(overrides: { return { face, update, replace, set } } +type WireFace = ConstructorParameters[0] + async function mountSection(overrides: Parameters[0] = {}) { const { face, update, replace, set } = scriptedFace(overrides) - const controller = new ModelsSettingsStore(face as never) + const controller = new ModelsSettingsStore(face as unknown as WireFace) await controller.load() const injected: ModelsSectionInjected = { controller, @@ -114,7 +116,7 @@ async function mountSection(overrides: Parameters[0] = {}) api: face as never, t, } - const view = render() + const view = render() return { view, face, update, replace, set, controller } } @@ -275,14 +277,14 @@ describe('ModelsSection', () => { it('renders the load failure with a retry control', async () => { const face = scriptedFace() face.face.llm.providers = vi.fn(() => Promise.resolve(fail('directory down', 'internal'))) as never - const controller = new ModelsSettingsStore(face.face as never) + const controller = new ModelsSettingsStore(face.face as unknown as WireFace) await controller.load() - render() + render() expect(screen.getByText(/directory down/)).toBeTruthy() fireEvent.click(screen.getByText(en.retry)) await waitFor(() => { expect(screen.queryByText(/directory down/)).toBeNull() }) @@ -294,15 +296,15 @@ describe('ModelsSection', () => { writable: false, namespaces: wireNamespaces(), }))) - const controller = new ModelsSettingsStore(face as never) + const controller = new ModelsSettingsStore(face as unknown as WireFace) await controller.load() cleanup() - render() + render() expect(screen.getByText(en.readOnly)).toBeTruthy() expect(screen.getAllByText(en.remove).every(button => button.disabled)).toBe(true) }) @@ -359,13 +361,13 @@ describe('ModelsSection', () => { it('loads on first render of an idle controller', async () => { const { face } = scriptedFace() - const controller = new ModelsSettingsStore(face as never) - render() + const controller = new ModelsSettingsStore(face as unknown as WireFace) + render() await screen.findByText('DeepSeek') }) diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 91b14f32d7..b235e2accb 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -21,6 +21,7 @@ "license": "BSD-3-Clause", "dependencies": { "@deepseek-ai/dsh-client-modules": "workspace:^", + "@deepseek-ai/dsh-client-schema-form": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-client-ui-theme": "workspace:^", diff --git a/packages/client/web/src/platform.ts b/packages/client/web/src/platform.ts index e51bc20eb9..dc6b9e58ed 100644 --- a/packages/client/web/src/platform.ts +++ b/packages/client/web/src/platform.ts @@ -10,6 +10,7 @@ export const PLATFORM_MODULES = [ '@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client-web-react', '@deepseek-ai/dsh-client-ui-primitives', + '@deepseek-ai/dsh-client-schema-form', ] as const /** One platform module specifier (a seed-table key). */ diff --git a/packages/client/web/src/seed.ts b/packages/client/web/src/seed.ts index ef66c4d7e3..11f976f4db 100644 --- a/packages/client/web/src/seed.ts +++ b/packages/client/web/src/seed.ts @@ -14,6 +14,7 @@ import * as Cordis from 'cordis' import * as UiSlots from '@deepseek-ai/dsh-client-ui-slots' import * as WebReact from '@deepseek-ai/dsh-client-web-react' import * as UiPrimitives from '@deepseek-ai/dsh-client-ui-primitives' +import * as SchemaForm from '@deepseek-ai/dsh-client-schema-form' import type { PlatformModule } from './platform.ts' /** @@ -33,5 +34,6 @@ export function getStaticModules(): Record { '@deepseek-ai/dsh-client-ui-slots': UiSlots, '@deepseek-ai/dsh-client-web-react': WebReact, '@deepseek-ai/dsh-client-ui-primitives': UiPrimitives, + '@deepseek-ai/dsh-client-schema-form': SchemaForm, } satisfies Record } diff --git a/packages/client/web/tsconfig.json b/packages/client/web/tsconfig.json index 203ad9c80b..9240c34891 100644 --- a/packages/client/web/tsconfig.json +++ b/packages/client/web/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../ui-primitives" }, + { + "path": "../schema-form" + }, { "path": "../web-react" }, diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index ed2b0a783a..9601f5c14e 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -76,7 +76,7 @@ const catalogModel: z = z.object({ export const Config: z = z.object({ apiKey: z.string().role('secret'), - apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV), + apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV), baseURL: z.string(), thinking: z.union(['enabled', 'disabled']), reasoningEffort: z.union(['off', 'high', 'max']), diff --git a/packages/llm/llm-pi-ai/src/config.ts b/packages/llm/llm-pi-ai/src/config.ts index 053d6d56e6..c635b1f13e 100644 --- a/packages/llm/llm-pi-ai/src/config.ts +++ b/packages/llm/llm-pi-ai/src/config.ts @@ -77,7 +77,7 @@ const thinkingBudgets = z.object({ const profile = z.object({ apiKey: z.string().role('secret'), - apiKeyEnv: z.string(), + apiKeyEnv: z.string().role('credential-ref'), baseURL: z.string(), headers: z.dict(z.string()), reasoning: z.union(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef17048745..8755e918b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,6 +209,9 @@ importers: '@deepseek-ai/dsh-compact-basic': specifier: workspace:^ version: link:../../packages/compact/compact-basic + '@deepseek-ai/dsh-credentials-local': + specifier: workspace:^ + version: link:../../packages/credentials/credentials-local '@deepseek-ai/dsh-frontend': specifier: workspace:^ version: link:../web @@ -236,6 +239,9 @@ importers: '@deepseek-ai/dsh-llm-deepseek': specifier: workspace:^ version: link:../../packages/llm/llm-deepseek + '@deepseek-ai/dsh-llm-pi-ai': + specifier: workspace:^ + version: link:../../packages/llm/llm-pi-ai '@deepseek-ai/dsh-llm-retry': specifier: workspace:^ version: link:../../packages/llm/llm-retry @@ -263,6 +269,9 @@ importers: '@deepseek-ai/dsh-session-title-first-message-llm': specifier: workspace:^ version: link:../../packages/session-title/session-title-first-message-llm + '@deepseek-ai/dsh-settings-local': + specifier: workspace:^ + version: link:../../packages/settings/settings-local '@deepseek-ai/dsh-skill': specifier: workspace:^ version: link:../../packages/skill/skill @@ -1612,6 +1621,9 @@ importers: '@deepseek-ai/dsh-client-modules': specifier: workspace:^ version: link:../modules + '@deepseek-ai/dsh-client-schema-form': + specifier: workspace:^ + version: link:../schema-form '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives diff --git a/tsconfig.host.json b/tsconfig.host.json index 6bfe4d2cad..383718dffa 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -16,6 +16,7 @@ "apps/web/tests/navigation-panes.e2e.ts", "apps/web/tests/lifecycle-chrome.e2e.ts", "apps/web/tests/settings-chrome.e2e.ts", + "apps/web/tests/models-settings.e2e.ts", "apps/web/tests/workspace-management.e2e.ts", "apps/web/tests/replay-round-trip.e2e.ts", "apps/web/tests/seeded-history.e2e.ts",