diff --git a/README.md b/README.md index 2ecc4ad..b0e0811 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ English | [中文](README.zh.md) - **`refkit_rights`** — re-check one license for a different intent without searching again. - **Web card** — thumbnail grid; license chip on every tile; green / blue / red / amber badges for allowed / credit required / not allowed / needs review; one-click credit copy. -Eleven sources work with no key at all (Openverse, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven, ambientCG, nailbook). Add free keys for the rest under Settings → Plugins → refkit. +Eleven sources work with no key at all (Openverse, Met, Art Institute of Chicago, Wikimedia Commons, Rijksmuseum, Internet Archive, Project Gutenberg, PoetryDB, Poly Haven, ambientCG, nailbook). Add free keys for the rest under **Plugins (sidebar) → refkit → Components → refkit**. ## Install @@ -18,11 +18,11 @@ dsh plugin --profile web add @refkit/dsh-plugin dsh plugin --profile web add github:refkitjs/dsh-plugin ``` -Restart the dsh web host once so the profile picks up the bundle. Tested against `@deepseek-ai/dsh` 0.1.5-rc.2 (the `next` channel); dsh is in developer preview and its plugin API changes between release candidates. +Restart the dsh web host once so the profile picks up the bundle. Tested against `@deepseek-ai/dsh` 0.1.7-rc.2. Requires dsh 0.1.x from 0.1.7-rc.2 on (`^0.1.7-rc.2`); dsh refuses to load it on older releases and on the 0.2.0 pre-releases (`next`) until support is widened. dsh is in developer preview and its plugin API changes between release candidates. ## Configuration -Settings → Plugins → **refkit** (namespace `refkit`; changes apply on the next call). Keys are `secret` fields: masked in the card, never logged, never returned to the model. Each key also falls back to an environment variable — the same names `@refkit/mcp` reads, so one `.env` serves both. +Keys and tuning are set on **Plugins (sidebar) → refkit → Components → refkit** (namespace `refkit`); changes apply on the next call, no restart needed. Keys are `secret` fields: never shown on the settings page (only a Set / Not set marker), never logged, never returned to the model, and stored in the profile's `cordis.patch.yml` (file mode 0600, plaintext). Each key left empty falls back to the environment variables in the table below (`REFKIT_*` first) — the same names `@refkit/mcp` reads, so one `.env` serves both. An invalid hand-edited `cordis.patch.yml` stops the plugin at startup until it's fixed. | Field | Env (first wins) | Enables | | --- | --- | --- | diff --git a/README.zh.md b/README.zh.md index e4ecabe..e0480a1 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,7 +8,7 @@ - **`refkit_rights`** — 针对不同的使用意图重新校验同一条许可证,无需重新搜索。 - **网页卡片** — 缩略图网格;每个卡片都带许可证标签;绿 / 蓝 / 红 / 黄四色徽章分别对应 allowed(允许) / credit required(需署名) / not allowed(不允许) / needs review(需人工复核);一键复制署名文本。 -十一个来源无需任何密钥即可使用(Openverse、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven、ambientCG、nailbook)。其余来源在「设置 → 插件 → refkit」里填入免费密钥即可启用。 +十一个来源无需任何密钥即可使用(Openverse、Met、Art Institute of Chicago、Wikimedia Commons、Rijksmuseum、Internet Archive、Project Gutenberg、PoetryDB、Poly Haven、ambientCG、nailbook)。其余来源在「插件(侧边栏)→ refkit → 包含的组件 → refkit」里填入免费密钥即可启用。 ## 安装 @@ -18,11 +18,11 @@ dsh plugin --profile web add @refkit/dsh-plugin dsh plugin --profile web add github:refkitjs/dsh-plugin ``` -重启一次 dsh web host,让该 profile 加载该 bundle。已在 `@deepseek-ai/dsh` 0.1.5-rc.2(`next` 频道)上测试通过;dsh 目前处于开发者预览阶段,其插件 API 在各个 release candidate 之间会发生变化。 +重启一次 dsh web host,让该 profile 加载该 bundle。已在 `@deepseek-ai/dsh` 0.1.7-rc.2 上测试通过;要求 dsh 0.1.x 且不低于 0.1.7-rc.2(`^0.1.7-rc.2`),更低版本以及 0.2.0 预发布版(`next` 频道)的 dsh 会拒绝加载该插件,直到后续版本放宽支持范围。dsh 目前处于开发者预览阶段,其插件 API 在各个 release candidate 之间会发生变化。 ## 配置 -Settings → Plugins → **refkit**(命名空间为 `refkit`;修改在下一次调用时生效)。所有 key 字段都是 `secret` 类型:在卡片中会被遮罩显示,不会被记录日志,也不会返回给模型。每个 key 都有对应的环境变量兜底——与 `@refkit/mcp` 读取的变量名相同,因此同一份 `.env` 可以同时服务这两者。 +密钥和调优参数在「插件(侧边栏)→ refkit → 包含的组件 → refkit」中设置(命名空间为 `refkit`);修改在下一次调用时生效,无需重启。所有 key 字段都是 `secret` 类型:设置页不会回显已保存的值(只显示 Set / Not set 标记),不会被记录日志,也不会返回给模型,并以明文形式存储在该 profile 的 `cordis.patch.yml` 中(文件权限 0600)。某个 key 留空时会依次回退读取下表列出的环境变量(`REFKIT_*` 优先)——与 `@refkit/mcp` 读取的变量名相同,因此同一份 `.env` 可以同时服务这两者。手动编辑后校验失败的 `cordis.patch.yml` 会使插件启动失败,直到修复为止。 | 字段 | 环境变量(先匹配者优先) | 启用的来源 | | --- | --- | --- | diff --git a/cordis.patch.yml b/cordis.patch.yml index c325080..e3c50b1 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -1,8 +1,9 @@ # @refkit/dsh-plugin bundle patch: one row inserted into the profile roster. # Install with `dsh plugin --profile web add @refkit/dsh-plugin` (npm) or # `dsh plugin --profile web add github:refkitjs/dsh-plugin`. Keys go into -# Settings -> Plugins -> refkit (or the REFKIT_* environment variables); the -# non-secret defaults below can be overridden here or in the settings card. +# Plugins (sidebar) → refkit → Components → refkit (or the environment +# variables listed in the README, REFKIT_* first); the non-secret defaults +# below can be overridden here or in the settings page. - insert: - id: refkit name: '@refkit/dsh-plugin' diff --git a/docs/superpowers/plans/2026-09-28-dsh-017-port.md b/docs/superpowers/plans/2026-09-28-dsh-017-port.md new file mode 100644 index 0000000..6b3011e --- /dev/null +++ b/docs/superpowers/plans/2026-09-28-dsh-017-port.md @@ -0,0 +1,236 @@ +# @refkit/dsh-plugin 0.2.0 — port to dsh 0.1.7 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `@refkit/dsh-plugin` fully functional on DeepSeek Harness 0.1.7-rc.2 — live settings with a native-looking settings page, corrected client typings and externals, smaller thumbnails — and release it as 0.2.0. + +**Architecture:** The host half reads its `Config` as dsh Loader `Volatile` references and rebuilds the refkit client on `loader/volatile-update`; validation moves into the schema. The browser half adds a `plugins.row.config` page built from dsh's shared `SettingsFormModel` and settings-form components, next to the existing `tool.call.toolview` card, which becomes phase-aware. + +**Tech Stack:** TypeScript 5.7, dsh 0.1.7-rc.2 package family, `@deepseek-ai/cordis` 4.0.4, `@deepseek-ai/schemastery` 3.18.4, React 18, tsdown 0.22, vitest, pnpm 10.32.1 (`npx -y pnpm@10.32.1`). + +**Spec:** `docs/superpowers/specs/2026-09-28-dsh-017-port-design.md` (P1–P6), amending `docs/superpowers/specs/2026-09-20-dsh-plugin-design.md`. API evidence: the research brief `.superpowers/research/2026-09-28-dsh-017-migration.md` (git-ignored; exists in this checkout) — cited below as §n / "Recommended migration step n". + +## Global Constraints + +- Package version `0.2.0`; `PLUGIN_VERSION = '0.2.0'`. +- DSH peers exactly: `@deepseek-ai/dsh-tools` `^0.1.7-rc.2`, `@deepseek-ai/dsh-system-prompt` `^0.1.7-rc.2`, `@deepseek-ai/dsh-settings` `^0.1.7-rc.2` (optional); plus `@deepseek-ai/cordis` `^4.0.4`, `@deepseek-ai/schemastery` `^3.18.4`, `react` `^18.2.0` (optional). +- No `pnpm.overrides` block. No `@deepseek-ai/dsh-client-runtime` anywhere. +- `CLIENT_EXTERNALS` and the bundle check allow-list are exactly: `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-store`, `@deepseek-ai/dsh-client-ui-slots`, `@deepseek-ai/dsh-client-ui-primitives`, `@deepseek-ai/dsh-client-ui-dockkit`. +- All 18 `Config` fields `.volatile()`; `sources` is `z.array(z.union(PROVIDER_IDS)).default([]).volatile()`; secrets keep `role('secret')`. +- Settings page slot: `plugins.row.config`, key `'@refkit/dsh-plugin#refkit'`, registrant `'@refkit/dsh-plugin'`. +- Settings location copy: EN "Plugins (sidebar) → @refkit/dsh-plugin → refkit → Configure"; ZH 「插件(侧边栏)→ @refkit/dsh-plugin → refkit → 配置」. +- Unchanged: tool names, parameters, output schemas, render text, presentation metadata, env fallback names, defaults and bounds, the 65 s tool timeout backstop, the `tool.call.toolview` key. +- Browser half: value imports from `@deepseek-ai/*` only from the externals list; everything else `import type`. Every wiring step guarded with try/catch + `console.warn`; nothing throws into the shell. +- Canonical values carry no `undefined`-valued properties; secrets never reach any output, log or error. +- Gate before every commit: `npx -y pnpm@10.32.1 typecheck && npx -y pnpm@10.32.1 lint && npx -y pnpm@10.32.1 test && npx -y pnpm@10.32.1 build && node scripts/check-client-bundle.mjs && node scripts/smoke-host.mjs`, pristine output, `git status --porcelain -- lib` empty after the build except what the commit includes. Conventional commits, no attribution trailers. Work in `/Users/xuan/Desktop/testSpace/dsh-plugin` on branch `feat/dsh-017-port`; never `git stash`, never push. + +--- + +### Task 1: dsh 0.1.7 toolchain, live configuration, client type port + +**Files:** +- Modify: `package.json`, `pnpm-lock.yaml`, `tsdown.config.ts`, `scripts/check-client-bundle.mjs`, `scripts/smoke-host.mjs` +- Modify: `src/config.ts`, `src/index.ts`, `src/client/index.tsx`, `src/client/Card.tsx` +- Test: `tests/config.test.ts`, `tests/index.test.ts` +- Regenerate: `lib/**` + +**Interfaces:** +- Produces (src/config.ts): `ConfigValues` (plain value interface — the former `Config` interface, with `sources?: readonly string[]`), `type Config` (`{ readonly [K in keyof ConfigValues]-?: Volatile }`), `const Config` (schemastery schema, all fields `.volatile()`), `readConfig(config: Config): ConfigValues`, `resolveConfig(values: ConfigValues, env?)` (behaviour unchanged). `validateConfig` is deleted. +- Produces (src/index.ts): `applyWith(ctx, config: Config, deps: ApplyDeps): PluginHandles` (unchanged signature shape; `config` is now references), `apply(ctx, config: Config)` (no default argument — the Loader always supplies references). +- Consumed later: Task 2 imports nothing from the host; Task 3 changes copy in `src/config.ts` and `src/tools/search.ts`. + +- [ ] **Step 1: package.json** + + - `version` → `0.2.0`; `description` → `DeepSeek Harness plugin for refkit: license-normalized creative reference search across 23 sources from 19 provider packages and 4 modalities, a strict-deny use-gate, and a web card with license and use-verdict badges (refkit_search / refkit_rights).` + - `peerDependencies` / `peerDependenciesMeta` per the Global Constraints (dsh-settings and react optional; drop dsh-system-prompt from optional only if it was — keep it optional as today). + - `devDependencies`: replace every `@deepseek-ai/*` entry with the 0.1.7 set: `@deepseek-ai/cordis` `~4.0.4`, `@deepseek-ai/cordis-plugin-loader` `~1.0.5`, `@deepseek-ai/schemastery` `~3.18.4`, and exactly `0.1.7-rc.2` for `dsh-tools`, `dsh-system-prompt`, `dsh-settings`, `dsh-client-ui-tool`, `dsh-client-ui-conversation`, `dsh-client-ui-renderer`, `dsh-client-ui-chat`, `dsh-client-ui-slots`, `dsh-client-ui-plugin-manager`, `dsh-client-ui-settings`, `dsh-client-ui-primitives`, `dsh-api-remotes`. Remove `@deepseek-ai/dsh-client-runtime`. Keep the non-dsh devDependencies. + - Delete the whole `"pnpm": { "overrides": … }` block. + - `dsh.client.inject` → `["@deepseek-ai/dsh-client-ui-tool", "@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-ui-plugin-manager", "@deepseek-ai/dsh-client-ui-settings"]`. + - Run `npx -y pnpm@10.32.1 install`; expect success with no `ERR_PNPM_NO_MATCHING_VERSION` (brief §6 probe `a/`). Paste any peer warnings into the report. + +- [ ] **Step 2: externals** + + `tsdown.config.ts`: set `CLIENT_EXTERNALS` to the Global Constraints list (as a `readonly string[]`). `scripts/check-client-bundle.mjs`: its `allowed` set becomes the `@deepseek-ai/*` subset of that list (`cordis`, `dsh-client-store`, `dsh-client-ui-slots`, `dsh-client-ui-primitives`, `dsh-client-ui-dockkit`). Add `@deepseek-ai/cordis-plugin-loader` and `@deepseek-ai/dsh-settings` to `HOST_EXTERNAL` defensively (type-only today). + +- [ ] **Step 3: failing config tests** + + In `tests/config.test.ts` (keep the existing registry/env/clamp/buildClient tests, adapting any `Config` interface usage to `ConfigValues`): + - Replace the `validateConfig` describe with: `expect(() => Config({ sources: ['unsplsh'] })).toThrow(/unsplsh/)` and the thrown message also matches `/wikimedia-commons/` (the union lists the valid ids); `expect(() => Config({ sources: ['met', 'unsplash'] })).not.toThrow()`. + - Add: `const values = readConfig(Config({}))` → `toMatchObject({ sources: [], limit: 12, poolFactor: 2, deadlineMs: 15000, timeoutMs: 10000, rerank: true, sourceConfidence: true })` and every `KEY_FIELDS` entry is `undefined`. + - Add: every field of `Config({})` exposes a `.get` function (`Object.values(Config({})).every(r => typeof (r as { get?: unknown }).get === 'function')`). + - Keep the secret-role test (every `KEY_FIELDS` field `role: 'secret'`, no other field) — adapt the `toJSON()` walk if volatile changes the node shape; add an assertion that every one of the 18 fields carries `meta.volatile === true` in the same walk. + - Keep the bounds tests (`Config({ limit: 0 })` etc. throw). + Run `npx -y pnpm@10.32.1 vitest run tests/config.test.ts` → expect FAIL (no `readConfig`, no volatile). + +- [ ] **Step 4: src/config.ts** + + Follow brief "Recommended migration step 2": + ```ts + import type { Volatile } from '@deepseek-ai/cordis' + /** Plain settings values: what each reference's `.get()` returns. */ + export interface ConfigValues { /* the 18 fields of today's Config interface, sources?: readonly string[] */ } + /** Config as `apply` receives it: every field a live Loader reference. */ + export type Config = { readonly [K in keyof ConfigValues]-?: Volatile } + const secret = (text: string) => z.string().role('secret').description(text).volatile() + // declared AFTER PROVIDER_REGISTRY / PROVIDER_IDS, because sources needs the id union + export const Config = z.object({ + unsplashAccessKey: secret('…same text as today…'), /* …the other nine… */ + sources: z.array(z.union(PROVIDER_IDS as unknown as [string, ...string[]])).default([]).description('…').volatile(), + limit: z.number().step(1).min(1).max(30).default(DEFAULTS.limit).description('…').volatile(), + /* poolFactor, deadlineMs (max MAX_DEADLINE_MS), timeoutMs, rerank, sourceConfidence: today's specs + .volatile() */ + userAgent: z.string().description('…').volatile(), + }) + const _schemaCheck: Config = {} as ReturnType; void _schemaCheck + /** One consistent snapshot; the Loader commits every reference before it emits the event. */ + export function readConfig(config: Config): ConfigValues { + return Object.fromEntries(Object.entries(config).map(([k, ref]) => [k, (ref as Volatile).get()])) as ConfigValues + } + ``` + `resolveConfig(values: ConfigValues, env = process.env)` keeps today's body (`Array.isArray(values.sources)` still works on a frozen array). Delete `validateConfig` and its export from `src/index.ts`. If `z.union` needs a mutable tuple type, cast as shown; keep `PROVIDER_IDS` itself `readonly string[]`. + Run the config tests → PASS. + +- [ ] **Step 5: failing index tests** + + Rewrite `tests/index.test.ts` around the new model (brief step 8): + - `fakeContext(services)` gains `on(name, fn)` storing listeners in a map and an `emit(name, ...args)` helper; `inject(deps, cb)` as today, with an optional `settings` service shaped `{ configure: (policy, owner) => () => void }` and the scope passed to `cb` having `effect(fn)` that calls `fn()` immediately. + - `liveConfig(get: () => ConfigValues): Config` builds an object whose 18 fields are `{ get: () => get()[field] }`. + - Tests: (1) `name`/`inject` unchanged; (2) both tools register with no optional services; (3) lazy client + live rebuild: `applyWith(ctx, liveConfig(() => current), { createClient: spy, env: {} })`, `getClient()` twice builds once without pexels, then set `current = { pexelsApiKey: 'k', limit: 7 }`, `emit('loader/volatile-update', [['pexelsApiKey'], ['limit']])`, `getConfig().limit === 7`, next `getClient()` builds a second client containing `pexels` and `pexels-video`, and `apply`-style registration happened exactly once (tools registered once); (4) with a `settings` service present, `configure` is called once with `{ auto: false }`; (5) system-prompt section unchanged (name `tool:refkit`, order 115, GUIDANCE); (6) GUIDANCE mentions `refkit_search`, `refkit_rights`, `intent`. + Run → FAIL. + +- [ ] **Step 6: src/index.ts** + + Brief step 3, plus the `configure` opt-out (spec P2): + ```ts + import type {} from '@deepseek-ai/cordis-plugin-loader' // 'loader/volatile-update' event typing + import type {} from '@deepseek-ai/dsh-settings' // ctx.settings.configure typing + export function applyWith(ctx: Context, config: Config, deps: ApplyDeps): PluginHandles { + const env = deps.env ?? process.env + let resolved = resolveConfig(readConfig(config), env) + let client: RefkitClient | null = null + ctx.on('loader/volatile-update', () => { resolved = resolveConfig(readConfig(config), env); client = null }) + const getConfig = (): ResolvedConfig => resolved + const getClient = (): RefkitClient => (client ??= buildClient(resolved, deps.createClient)) + ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) + ctx.inject(['systemPrompt'], (scope) => { scope.systemPrompt.section({ name: 'tool:refkit', order: 115, text: GUIDANCE }) }) + ctx.tools.register(createSearchTool({ client: getClient, config: getConfig })) + ctx.tools.register(createRightsTool()) + return { getClient, getConfig } + } + export function apply(ctx: Context, config: Config): void { applyWith(ctx, config, { createClient: createRefkit }) } + ``` + Update the module doc comment (no "settings section" wording). Re-exports: `Config`, `readConfig`, types `Config as RefkitPluginConfig`, `ConfigValues`, `ResolvedConfig`; drop `validateConfig`. If the fake `ctx.fiber` is undefined in tests, `configure` receives undefined — acceptable; assert only the policy argument. + Run the index tests → PASS. + +- [ ] **Step 7: client type port** + + `src/client/index.tsx`: `import type { Context as ClientContext } from '@deepseek-ai/cordis'`; `import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'` (ctx.slots); keep the ui-tool and ui-conversation type imports; wiring unchanged. + `src/client/Card.tsx`: `import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client'`; `RefkitCard(props: ToolCallOwnerProps)` returns `` when `props.phase !== 'result'`, otherwise `const block = props.block` and the existing settled logic; keep `textOf`/`outcomeOf` working on the result node type (their `'kind' in block` guards may stay or be simplified; typecheck is the arbiter). + +- [ ] **Step 8: smoke script** + + `scripts/smoke-host.mjs` live part: give the fake `ctx` an `on: () => () => {}` stub and call `apply(ctx, Config({}))`. The surface check stays as is. + +- [ ] **Step 9: gate, build, commit** + + Run the full gate from Global Constraints. `lib/` regenerates (types, index.js, client.js). Commit: `feat!: port to dsh 0.1.7 — volatile live config, schema-level validation, client type imports, corrected externals`. + +--- + +### Task 2: settings page on the Plugins page + +**Files:** +- Create: `src/client/settings-model.ts`, `src/client/SettingsPage.tsx` +- Modify: `src/client/index.tsx`, `src/client/copy.ts`, `src/client/styles.ts` (only if a layout class is needed) +- Test: `tests/settings-model.test.ts` +- Regenerate: `lib/**` + +**Interfaces:** +- Consumes: `KEY_FIELDS` and `PROVIDER_REGISTRY` data — NOT by importing `src/config.ts` into the client bundle (it pulls 19 provider packages). Instead `settings-model.ts` holds a small static table `KEY_LABELS: Record` (field → providers it enables, e.g. `pexelsApiKey: 'Pexels (images, video)'`) and `SOURCE_IDS: readonly string[]` (the 23 ids); a test asserts both match `KEY_FIELDS` / `PROVIDER_IDS` from `src/config.ts` so they cannot drift. +- Produces: `settingsBooleanField(field)`, `settingsSourcesField(field, ids)` (both `SettingsFieldSpec`-compatible: `{ field, format(value) → string, parse(text) → SettingsFieldWrite | undefined }`), `secretSpec(field, write)`, `summaryText(configuredCount, total)`, `KEY_LABELS`, `SOURCE_IDS`; `RefkitSettingsPage` component; `createSettingsController(configForms)`. + +Reference implementation to mirror: `@deepseek-ai/dsh-client-ui-settings-web-search` (installed at `$(npm root -g)/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-ui-settings-web-search/lib/client.js`; source at `https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.1.7-rc.2/packages/client/ui-settings-web-search/src`). Read it first. Also read the typings in `node_modules/@deepseek-ai/dsh-client-ui-primitives/lib/types/settings-form/*.d.ts`, `node_modules/@deepseek-ai/dsh-client-ui-settings/lib/types/client/config-form*.d.ts`, and `node_modules/@deepseek-ai/dsh-client-ui-plugin-manager/lib/types/client/slot-contract.d.ts`. + +- [ ] **Step 1: failing settings-model tests** (`tests/settings-model.test.ts`) + - `settingsBooleanField('rerank')`: `format(true) === 'on'`, `format(false) === 'off'`, `format(undefined) === ''`; `parse('on') → { kind: 'set', value: true }`, `parse('off') → { kind: 'set', value: false }`, `parse('') → { kind: 'clear' }`, `parse('maybe') → undefined`. + - `settingsSourcesField('sources', SOURCE_IDS)`: `format(['met','artic']) === 'met, artic'`, `format([]) === ''`; `parse('met, artic') → { kind: 'set', value: ['met','artic'] }`; `parse(' met ,, artic ') → same`; `parse('') → { kind: 'clear' }`; `parse('met, unsplsh') → undefined`; duplicates collapse (`parse('met, met')` → `['met']`). + - `secretSpec('pexelsApiKey', write)`: `.field === 'pexelsApiKey'`; `.write('k')` calls `write` with `[{ op: 'set', path: ['pexelsApiKey'], value: 'k' }]` and resolves to its boolean. + - `summaryText(3, 10) === 'License-aware reference search · 3 of 10 keys set'`. + - `Object.keys(KEY_LABELS).sort()` equals `[...KEY_FIELDS].sort()`; `[...SOURCE_IDS].sort()` equals `[...PROVIDER_IDS].sort()` (import these two from `../src/config.ts` in the test only). + Run → FAIL. + +- [ ] **Step 2: src/client/settings-model.ts** — implement the above (DOM-free, no `@deepseek-ai/*` value imports; import `SettingsFieldSpec`/`SettingsFieldWrite`/`SettingsSecretSpec`/`SettingsFormPathOp` types from `@deepseek-ai/dsh-client-ui-primitives` with `import type`). Run → PASS. + +- [ ] **Step 3: controller + page** + + - Controller (in `SettingsPage.tsx` or a sibling `settings-controller.ts`): given `configForms` (the injected service), `const form = configForms.get('refkit')` (confirm the accessor name from the typings/reference), `new SettingsFormModel(form, [settingsNumberField('limit'), settingsNumberField('poolFactor'), settingsNumberField('deadlineMs'), settingsNumberField('timeoutMs'), settingsTextField('userAgent'), settingsBooleanField('rerank'), settingsBooleanField('sourceConfidence'), settingsSourcesField('sources', SOURCE_IDS)], KEY_FIELDS_CLIENT.map(f => secretSpec(f, ops => form.mutate(ops))))`, where `KEY_FIELDS_CLIENT = Object.keys(KEY_LABELS)`. Secret presence: read `configForms.describe().getSnapshot().view?.namespaces.find(n => n.ns === 'refkit')?.secrets` and expose `configured(field)` (`secrets.find(s => s.path[0] === field)?.set === true`); re-read on the describe store's changes. `remove(field)` performs `form.mutate([{ op: 'unset', path: [field] }])` immediately. + - Page `RefkitSettingsPage({ view }: PluginConfigViewProps)`: `view === 'summary'` → `summaryText(configuredCount, 10)`; `view === 'page'` → `SettingsForm` (labels from `COPY.settings`) containing an "API keys" group of 10 `SettingsSecretField`s (label `KEY_LABELS[field]`, hint the env names from a static table `KEY_ENV_HINT` in settings-model — e.g. `Env: REFKIT_PEXELS_KEY / PEXELS_KEY` — asserted equal to `KEY_ENV` in the test, `configured`, `stateLabel` "Set" / "Not set", and a `Button variant="ghost" size="sm"` "Remove" beside configured keys) and a "Search" group of `SettingsValueField`s (numbers with `numeric`, hints showing bounds; `sources` with placeholder "empty = all enabled sources" and `help` listing the ids; `userAgent`) plus two `Switch`es wired through `actions.edit(field, next ? 'on' : 'off')`. Read form state through the model's `bind(...)` store with `useSyncExternalStore`. + - If the controller cannot be created (no `configForms`), the page renders `COPY.settings.unavailable` inside a plain div. + +- [ ] **Step 4: registration** (`src/client/index.tsx`) + + Inside the existing `ctx.inject(['slots'], scope => …)`, add a second guarded registration: `scope.slots.inject('plugins.row.config', () => { try { return scope.slots.register({ name: 'plugins.row.config', key: '@refkit/dsh-plugin#refkit', registrant: '@refkit/dsh-plugin' }, RefkitSettingsPage) } catch (e) { console.warn('[refkit] settings page registration failed', e); return () => {} } })`. Create the controller in an optional `ctx.inject(['configForms'], child => …)` child (never a required top-level inject — brief §4 "Client inject"), store it where the page reads it (module-level holder set/cleared by the child's effect), and dispose it on unload. Prefer `configForms.whileServed('refkit', …)` for registering the page if the service offers it (as the web-search page does); otherwise register unconditionally and let the page show the unavailable line. + +- [ ] **Step 5: gate, build, commit** + + Full gate. `node scripts/check-client-bundle.mjs` must still pass — the only new runtime import is `@deepseek-ai/dsh-client-ui-primitives` (seed). Commit: `feat(client): settings page on the Plugins page — API keys and search tuning, live`. + +--- + +### Task 3: phase-aware card, smaller thumbnails, copy and docs + +**Files:** +- Modify: `src/client/Card.tsx`, `src/client/copy.ts`, `src/config.ts`, `src/tools/search.ts`, `cordis.patch.yml`, `README.md`, `README.zh.md`, `docs/superpowers/specs/2026-09-20-dsh-plugin-design.md` (a one-line pointer at the top to the port spec) +- Test: `tests/search.test.ts`, `tests/config.test.ts` +- Regenerate: `lib/**` + +- [ ] **Step 1: card phases** — `props.phase === 'preparing'` renders a one-line `
{COPY.preparing}
` with `COPY.preparing = 'Preparing refkit search…'`; `'start'` keeps `RunningGrid`. + +- [ ] **Step 2: thumbnails** — first confirm live: `curl -s "https://commons.wikimedia.org/w/api.php?action=query&format=json&generator=search&gsrsearch=lion&gsrnamespace=6&gsrlimit=2&prop=imageinfo&iiprop=url&iiurlwidth=500"` and check the returned `thumburl` width is 500 (paste the evidence). Then the registry entry becomes `make: () => wikimediaCommons({ thumbWidth: 500 })`; add a config test that the built provider… (if the provider does not expose its config, assert through a `vi.mock` spy on `wikimediaCommons` as the museum-cap tests do) receives `{ thumbWidth: 500 }`. + +- [ ] **Step 3: copy** — replace every "Settings -> Plugins -> refkit" string with the Global Constraints EN location (in `src/config.ts` `buildClient` error, `src/tools/search.ts` description and error hints, `src/client/copy.ts` if present); update the tests that match the old text (`tests/search.test.ts`, `tests/config.test.ts`) to match `/Plugins \(sidebar\) → @refkit\/dsh-plugin → refkit → Configure/`. `cordis.patch.yml` comments point to the same location. + +- [ ] **Step 4: README.md / README.zh.md** + - Install: unchanged commands; "Tested against `@deepseek-ai/dsh` 0.1.7-rc.2. Requires dsh 0.1.x from 0.1.7-rc.2 on (`^0.1.7-rc.2`); dsh refuses to load it on older releases and on the 0.2.0 pre-releases (`next`) until support is widened. dsh is in developer preview and its plugin API changes between release candidates." + - Configuration: keys and tuning are set on **Plugins (sidebar) → @refkit/dsh-plugin → refkit → Configure**; changes apply on the next call without restart; keys are stored in the profile's `cordis.patch.yml` (file mode 0600, plaintext) — the `REFKIT_*` environment variables remain a fallback for a key left empty; an invalid hand-edited `cordis.patch.yml` stops the plugin at startup until fixed. + - Keep every table; fix the description sentence to "23 sources from 19 provider packages". + - ZH mirrors EN with the ZH location string. + +- [ ] **Step 5: gate, build, commit** — `feat: phase-aware card, 500px Wikimedia thumbnails, settings location copy and docs for dsh 0.1.7`. + +--- + +### Task 4: refkit patch dependencies (execute only after npm publish) + +**Precondition:** `npm view @refkit/provider-openverse version` prints `0.5.1` and `npm view @refkit/provider-artic version` prints `0.4.1`. If not, report `BLOCKED` naming refkitjs/refkit's failed Release run (expired `NPM_TOKEN`); do not use tarballs or `file:` specs. + +- [ ] **Step 1:** `package.json` dependencies `@refkit/provider-openverse` `^0.5.1`, `@refkit/provider-artic` `^0.4.1`; `npx -y pnpm@10.32.1 install`; confirm the lockfile resolves 0.5.1 / 0.4.1. +- [ ] **Step 2:** live check (network): a one-off `node` script in the scratch area (not committed) that runs `runSearch({ query: 'neon street night' }, deps)` with `resolveConfig({ sources: ['openverse'] }, {})` via the built `lib/index.js` and prints the Openverse status — expect `fulfilled` with results (before the bump it was `failed … 401`). Paste the output. +- [ ] **Step 3:** full gate, commit `fix(deps): refkit provider patches — Openverse anonymous page size, ARTIC limit cap`. + +--- + +### Task 5: plugin display metadata and on-screen location copy (spec P7; added after live acceptance) + +**Files:** +- Create: `locale/en.json`, `locale/zh.json`, `icon.svg`, `tests/package-meta.test.ts` +- Modify: `package.json` (`icon`, `exports`, `files`), `src/config.ts`, `src/tools/search.ts`, `cordis.patch.yml`, `README.md`, `README.zh.md`, `tests/config.test.ts`, `tests/search.test.ts` +- Regenerate: `lib/**` + +- [ ] **Step 1: packaging test (RED)** — `tests/package-meta.test.ts` reads both locale files and `package.json` from the repo root and asserts: each locale file is `{ meta: { title: 'refkit', description } }` with a non-empty description containing `23`; `exports['./locale/*.json'] === './locale/*.json'`; `icon === './icon.svg'` and the file exists and starts with `&1 | grep -E "locale/|icon.svg"` lists all three (paste it). +- [ ] **Step 3: location copy** — replace every "Plugins (sidebar) → @refkit/dsh-plugin → refkit → Configure" with "Plugins (sidebar) → refkit → Components → refkit", and every 「插件(侧边栏)→ @refkit/dsh-plugin → refkit → 配置」 with 「插件(侧边栏)→ refkit → 包含的组件 → refkit」 (src, `cordis.patch.yml` comment, READMEs, and the four test regexes). The grep for the old strings outside `docs/superpowers` and `.superpowers` must return nothing. +- [ ] **Step 4: gate, build, commit** — `feat: plugin display metadata (title, localized description, icon) and on-screen settings location`. + +--- + +## Controller acceptance (after Task 4, before the final review) + +Live on dsh 0.1.7-rc.2 in the Browser pane, with the plugin installed from this branch (`dsh plugin --profile web add file:/Users/xuan/Desktop/testSpace/dsh-plugin`, host restarted): + +1. Plugins → @refkit/dsh-plugin → the `refkit` row shows a configure control; the page renders both groups populated with defaults, "Not set" on all keys. +2. A tuning edit (e.g. `limit` 8) saves, survives a page reload, and applies to the next search without restart (the card header shows 8 references when enough exist). +3. An invalid draft (`limit` 99, `sources` `met, bogus`) blocks save or is refused with the form's failure line; nothing is written. +4. Saving a dummy key for a keyed source flips it to "Set" and the source appears in the card's source chips (it will fail upstream with a bogus key — that is expected and shown as a failed source); **Remove** flips it back. +5. A search with `intent: commercial-product` renders the phase states and the result card; Openverse is fulfilled. +6. Summary line on the bundle page reads "… N of 10 keys set". +Record results in the ledger; any failure re-opens the owning task. diff --git a/docs/superpowers/specs/2026-09-20-dsh-plugin-design.md b/docs/superpowers/specs/2026-09-20-dsh-plugin-design.md index 467537a..15ef78b 100644 --- a/docs/superpowers/specs/2026-09-20-dsh-plugin-design.md +++ b/docs/superpowers/specs/2026-09-20-dsh-plugin-design.md @@ -1,5 +1,7 @@ # @refkit/dsh-plugin — Design Spec +Amended by `2026-09-28-dsh-017-port-design.md` (the dsh 0.1.7 port; sections not mentioned there stand unchanged). + Status: approved in conversation 2026-09-20; written for review. First release 0.1.0. ## Goals diff --git a/docs/superpowers/specs/2026-09-28-dsh-017-port-design.md b/docs/superpowers/specs/2026-09-28-dsh-017-port-design.md new file mode 100644 index 0000000..702508c --- /dev/null +++ b/docs/superpowers/specs/2026-09-28-dsh-017-port-design.md @@ -0,0 +1,179 @@ +# @refkit/dsh-plugin 0.2.0 — port to DeepSeek Harness 0.1.7 (design spec) + +Status: approved in conversation 2026-09-28. Amends `2026-09-20-dsh-plugin-design.md`; sections not +mentioned here stand unchanged. Evidence for every API claim: the research brief +`.superpowers/research/2026-09-28-dsh-017-migration.md` (git-ignored; its sections are cited as §n). + +## Why + +dsh 0.1.7-rc.2 (npm `latest`) removed `ctx.settings.installSection` and the +`@deepseek-ai/dsh-client-runtime` package. The 0.1.0 plugin loads and its tools work, but its +settings never register (the inject child throws), so users cannot enter API keys in the UI. +Live acceptance on 0.1.7-rc.2 also showed 1280 px thumbnails in 150 px tiles and a stale "19 +sources" description. + +## Goals + +1. Full function on dsh 0.1.7-rc.2: tools, card, and a settings page where users enter the 10 API + keys and the tuning fields, applied live without restart. +2. Declare compatibility honestly: dsh `^0.1.7-rc.2` only. +3. Remove the 0.1.5-era scaffolding that 0.1.7 makes unnecessary (the 18 `pnpm.overrides`). + +## Non-goals + +- dsh 0.2.0-rc.x support (published 2026-09-28; identical typings per §5, widen after a live smoke). +- `role('credential-ref')` indirection for keys (follow-up hardening; §Open risks). +- Any change to tool contracts, render text, presentation metadata, or the card's visual design + beyond the phase-aware running state. + +## Decisions + +### P1 — Version and peers (amends D1, D10) + +- Package version `0.2.0`; `PLUGIN_VERSION` follows. +- `peerDependencies`: `@deepseek-ai/dsh-tools` `^0.1.7-rc.2`, `@deepseek-ai/dsh-system-prompt` + `^0.1.7-rc.2`, `@deepseek-ai/dsh-settings` `^0.1.7-rc.2` (optional, used only for `configure`), + `@deepseek-ai/cordis` `^4.0.4`, `@deepseek-ai/schemastery` `^3.18.4`, `react` `^18.2.0` + (optional). dsh checks only `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peers, with + `includePrerelease` (§3), so `^0.1.7-rc.2` admits 0.1.7-rc.2 … 0.1.x and refuses 0.1.5/0.1.6 + (where `.volatile()` does not exist) and 0.2.0-rc.x. +- `devDependencies` pinned exactly to the 0.1.7-rc.2 family (§6 list): `dsh-tools`, + `dsh-system-prompt`, `dsh-settings`, `dsh-client-ui-tool`, `dsh-client-ui-conversation`, + `dsh-client-ui-renderer`, `dsh-client-ui-chat`, `dsh-client-ui-slots`, + `dsh-client-ui-plugin-manager`, `dsh-client-ui-settings`, `dsh-client-ui-primitives`, + `dsh-api-remotes` at `0.1.7-rc.2`; `cordis` `~4.0.4`; `cordis-plugin-loader` `~1.0.5` + (type-only, for the `loader/volatile-update` event); `schemastery` `~3.18.4`. + `@deepseek-ai/dsh-client-runtime` is removed. The whole `pnpm.overrides` block is deleted — a + 0.1.7 dev install resolves without it (§6). +- `dsh.client.inject` becomes `['@deepseek-ai/dsh-client-ui-tool', + '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-plugin-manager', + '@deepseek-ai/dsh-client-ui-settings']` (informational in 0.1.7, §4). +- `description`: "… across 23 sources from 19 provider packages and 4 modalities …". +- README "tested against" becomes `@deepseek-ai/dsh` 0.1.7-rc.2. + +### P2 — Live configuration (replaces D4's settings mechanism) + +- Every one of the 18 `Config` fields is declared `.volatile()`; secrets keep `role('secret')`. +- `sources` becomes `z.array(z.union(PROVIDER_IDS)).default([]).volatile()`; the schema rejects an + unknown id with a message that lists the valid ids. `validateConfig` is deleted. Retired ids + must stay in the union as no-ops in future releases (§2). +- Types: `ConfigValues` is the plain value interface (what the old `Config` interface was); + `Config` (type) maps each field to `Volatile<…>` from `@deepseek-ai/cordis`; `Config` (value) is + the schemastery schema. `readConfig(config): ConfigValues` reads every reference with `.get()`. + `resolveConfig(values, env)` is unchanged in behaviour and now takes `ConfigValues`. +- `apply(ctx, config)`: resolve once; register `ctx.on('loader/volatile-update', …)` on the + plugin's own context (the event is delivered to the owning fiber only) to re-resolve and drop + the cached client; the client is still built lazily. The environment is still re-read at every + resolve, so the `REFKIT_*` variables keep working as a fallback under empty settings. +- `ctx.inject(['settings'], child => child.effect(() => child.settings.configure({ auto: false }, + ctx.fiber)))` opts out of any future auto-generated page, because the plugin ships its own (P3). +- An edit to a volatile field never re-applies the plugin; a stored invalid config fails the + fiber at startup (the user fixes `cordis.patch.yml`) — documented in the README. +- User-facing copy that names the settings location reads: "Plugins (sidebar) → refkit → + Components → refkit" (EN) / 「插件(侧边栏)→ refkit → 包含的组件 → refkit」 (ZH), in tool errors, + `cordis.patch.yml` comments and READMEs. (Amended by P7 after live acceptance; the first draft's + "@refkit/dsh-plugin → refkit → Configure" did not match the screen.) + +### P3 — Settings page (new; replaces D4's settings card) + +The page follows the pattern of dsh's own settings pages (`@deepseek-ai/dsh-client-ui-settings-web-search`, +`-shell`, `-agent-loop`, `-subagent`), so it looks and behaves like the rest of the Plugins page. + +- **Where**: keyed slot `plugins.row.config`, key `'@refkit/dsh-plugin#refkit'`, registrant + `'@refkit/dsh-plugin'` — the `refkit` row on the `@refkit/dsh-plugin` bundle page gains a + configure control. Registered from the browser half inside the existing guarded + `ctx.inject(['slots'], …)` wiring, and only while the Host serves namespace `refkit` + (`ctx.configForms.whileServed` when available). +- **State**: a controller built in an optional `ctx.inject(['configForms'], …)` child binds + `configForms.get('refkit')` and stages edits with the shared `SettingsFormModel` exported by + `@deepseek-ai/dsh-client-ui-primitives` (a platform seed module; value imports allowed). Field + specs: `settingsNumberField` for `limit`, `poolFactor`, `deadlineMs`, `timeoutMs`; + `settingsTextField` for `userAgent`; refkit-owned specs (pure, in `src/client/settings-model.ts`) + for the booleans `rerank` / `sourceConfidence` and for `sources` (comma-separated ids, validated + against the 23 provider ids; empty = all). Secret specs for the 10 keys write + `{ op: 'set', path: [field], value: text }` through the same form's `mutate`. +- **Rendering**: `SettingsForm` frame (its own Save, saving, failed and read-only states) with two + groups: **API keys** — one `SettingsSecretField` per key, labelled with the providers it enables, + blank on load, a blank draft keeps the stored key, `configured` from the namespace's secret + presence markers (`configForms.describe()` → `namespaces.find(ns === 'refkit').secrets`), plus a + small **Remove** button beside a configured key that immediately writes `{ op: 'unset', path: + [field] }`; **Search** — `SettingsValueField`s for the numbers, `sources` and `userAgent` (each + with the Overridden badge and Reset that the component provides) and two `Switch`es. +- **Summary view** (`view: 'summary'`): "License-aware reference search · N of 10 keys set". +- **Copy**: all strings in `src/client/copy.ts` (English); schema bounds shown as hints. +- **Degradation**: no configForms service, namespace not served, or fiber failed → the page shows + the form frame's unavailable line; nothing throws into the shell. +- **Pure logic** in `src/client/settings-model.ts` (DOM-free, unit-tested): the boolean and + `sources` field specs (format/parse, invalid ids block the save), the secret spec factory, the + key → providers label table, and the summary text. + +### P4 — Client types and bundle (amends D5, D9) + +- `ClientContext` → `Context` from `@deepseek-ai/cordis`; `ctx.slots` typing from + `@deepseek-ai/dsh-client-ui-renderer/client`; `ToolCallBlock` from + `@deepseek-ai/dsh-client-ui-conversation/client`. +- `RefkitCard` narrows on `props.phase`: `'preparing'` renders a one-line "Preparing refkit + search…", `'start'` renders the existing skeleton grid, `'result'` the existing logic. +- `CLIENT_EXTERNALS` (and the bundle check's allow-list) become exactly the dsh web shell's seed + table: `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client`, `@deepseek-ai/cordis`, + `@deepseek-ai/dsh-client-store`, `@deepseek-ai/dsh-client-ui-slots`, + `@deepseek-ai/dsh-client-ui-primitives`, `@deepseek-ai/dsh-client-ui-dockkit` (§4). + +### P5 — Thumbnail size + +- The registry builds Wikimedia Commons with `thumbWidth: 500` (a standard Wikimedia thumbnail + step), so tiles load a ~500 px image instead of 1280 px. The implementer confirms live that the + API returns a 500-wide `thumburl`. + +### P6 — refkit patch dependency + +- `@refkit/provider-openverse` `^0.5.1` and `@refkit/provider-artic` `^0.4.1` (anonymous Openverse + `page_size` ≤ 20; ARTIC `limit` ≤ 100 — refkitjs/refkit#29). Applied only after both are on npm. + +### P7 — Plugin display metadata (added after live acceptance) + +- Observed on dsh 0.1.7-rc.2: without metadata the Plugins page falls back to the module + specifier. The installed row and the component row both read "@refkit/dsh-plugin"; the + component row shows the row id `refkit` as small code text; the configure control is the whole + component row (accessible name "配置 @refkit/dsh-plugin" / "Configure @refkit/dsh-plugin"), with + no visible "Configure" label. +- dsh reads display metadata without importing the plugin (`dsh-app-boot` `readPluginMeta`): + `/locale/en.json` (English required) and sibling locale files shaped + `{ "meta": { "title", "description" } }`, resolved through the package's `exports`, plus + `package.json.icon` loaded as an image data URL. Official plugins + (`@deepseek-ai/dsh-experimental-auto-review`) ship `locale/en.json`, `locale/zh.json` and + `icon.svg`, export `./locale/*.json`, and list all three in `files`. +- Ship the same: `locale/en.json` and `locale/zh.json`, both with title `refkit`; descriptions + EN "Search 23 open sources for image, video, audio and text references, each tagged with its + license and a use verdict." / ZH 「在 23 个开放来源中检索图片、视频、音频和文本参考素材,并为每条 + 结果标注许可证和可用性判定。」; a 36×36 `icon.svg` in the official style (transparent + background, one gradient-filled glyph, white strokes); `package.json` gains `"icon": + "./icon.svg"`, export `"./locale/*.json": "./locale/*.json"`, and `files` entries + `locale/*.json` and `icon.svg`. `package.json.description` stays as the npm description. +- Because the title equals the row id, the component row drops its code line. The location copy + in P2 is rewritten to match the screen. + +## Testing + +- Unit: `Config({})` yields references with the `DEFAULTS` values; `Config({ sources: ['unsplsh'] + })` throws naming the valid ids; `readConfig` snapshots; the index test drives a fake context + with `on('loader/volatile-update')` and a `liveConfig` helper whose references read a mutable + object, proving a settings edit swaps the client without re-apply; `configure({ auto: false })` + is registered when a settings service exists; settings-model diff/validation/summary. +- Build: bundle check with the corrected allow-list; `lib/` fresh. +- Packaging (P7): a unit test reads `locale/en.json` / `locale/zh.json` (both `meta.title === + 'refkit'`, non-empty descriptions mentioning 23 sources) and asserts `package.json` exports + `./locale/*.json`, sets `icon`, and lists `locale/*.json` and `icon.svg` in `files`; + `npm pack --dry-run` shows the three files. +- Live acceptance on dsh 0.1.7-rc.2 (controller, in the Browser pane): the Plugins page shows + the title `refkit` with the localized description and icon, the component row opens the page, + the page renders populated, saving a key flips its marker to "set" without restart and enables + the source on the next search, clearing it flips back, an invalid bound is refused with the + inline error, the card renders across phases, Openverse returns results. + +## Open risks + +- The `plugins.row.config` key format and the row id are documented, not yet observed live (§Open + risks); acceptance verifies them. +- Keys are stored in plaintext in the profile's `cordis.patch.yml` (mode 0600), as with every + volatile secret in 0.1.7; README says so. diff --git a/eslint.config.mjs b/eslint.config.mjs index 2f88e06..a371ae5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,7 @@ import tseslint from 'typescript-eslint' export default tseslint.config( - { ignores: ['lib/**', 'node_modules/**', 'docs/**'] }, + { ignores: ['lib/**', 'node_modules/**', 'docs/**', '.superpowers/**'] }, ...tseslint.configs.recommended, { rules: { diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..6c2952a --- /dev/null +++ b/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/client.js b/lib/client.js index ae98519..ab242bc 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,6 +6,7 @@ window.__ModuleLoader__.load({ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); let react = require("react"); let react_jsx_runtime = require("react/jsx-runtime"); + let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); //#region src/core/outcome.ts /** * Canonical result vocabulary shared by the host half (tool output, render, @@ -180,8 +181,9 @@ window.__ModuleLoader__.load({ } //#endregion //#region src/client/copy.ts - /** UI strings for the card. One object so a locale swap is one file. */ + /** UI strings for the card and the settings page. One object so a locale swap is one file. */ const COPY = { + preparing: "Preparing refkit search…", searching: "Searching refkit sources…", refsFor: (count, query) => `${count} reference${count === 1 ? "" : "s"} for “${query}”`, intent: (intent) => `intent: ${intent}`, @@ -194,13 +196,55 @@ window.__ModuleLoader__.load({ copyFailed: "Select and copy:", empty: "No results. Try broader terms, another modality, or fewer controls.", legend: "Verdict:", - untitled: "(untitled)" + untitled: "(untitled)", + settings: { + summary: (configured, total) => `License-aware reference search · ${configured} of ${total} keys set`, + unavailable: "refkit is not running, so it cannot be configured right now.", + readOnly: "This deployment stores settings read-only.", + saveFailed: "The deployment did not accept these values; they were left for you to correct.", + save: "Save", + saving: "Saving…", + overridden: "Overridden", + reset: "Reset to default", + keysHeading: "API keys", + keysNote: "Each key enables the sources named beside it; keyless sources need none. Keys are stored in plain text in this profile’s cordis.patch.yml (file mode 0600) and apply on the next search.", + keyHint: (env) => `Leave blank to keep the stored key. Environment fallback: ${env.join(" / ")}.`, + keySet: "Set", + keyUnset: "Not set", + remove: "Remove", + removeLabel: (label) => `Remove the ${label} key`, + removing: "Removing…", + removeBlocked: "Save your other edits first: removing a key writes immediately.", + removeFailed: "Not removed; try again.", + searchHeading: "Search", + limit: "Results per call", + limitHint: "Also caps per-item detail fetches for met, rijksmuseum and polyhaven.", + poolFactor: "Fusion pool factor", + poolFactorHint: "Rank-fusion pool multiplier.", + deadlineMs: "Search deadline (ms)", + deadlineMsHint: "Whole-search deadline.", + timeoutMs: "Per-source timeout (ms)", + timeoutMsHint: "How long one source may take.", + range: (hint, bounds) => `${hint} ${bounds.min}–${bounds.max}, default ${bounds.default}.`, + invalidRange: (bounds) => `Enter a whole number from ${bounds.min} to ${bounds.max}, or leave blank for the default.`, + sources: "Sources", + sourcesHint: "Comma-separated provider ids. A keyed source also needs its key.", + sourcesPlaceholder: "empty = all enabled sources", + sourcesHelp: "Source ids", + sourcesInvalid: "Unknown source id. Use the ids listed under the info button.", + userAgent: "User-Agent", + userAgentHint: "Sent with provider requests. Blank uses refkit-dsh-plugin/.", + rerank: "Lexical rerank", + rerankHint: "Rerank fused results over title, description, tags and excerpt.", + sourceConfidence: "Source confidence", + sourceConfidenceHint: "Down-weight sources whose batch never mentions the query." + } }; //#endregion //#region src/client/styles.ts /** - * Card stylesheet, injected once as