From 5411bc7c25838ccab8dcdfe602cbe7157c17ca02 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Thu, 27 Aug 2026 10:55:38 +0800 Subject: [PATCH 01/11] feat(fs): extraRoots config opens paths outside the session workspace File routes (fs.tree/fs.read/fs.write, media, HTML preview, upload) accept extra roots beyond the session cwd. Default: ~/.dsh/external when it exists; explicit [] disables; '~' expands; non-absolute entries fail loudly. The symlink-escape guard and the 403 contract are unchanged; fs.search stays workspace-only. --- README.md | 24 ++++ README_EN.md | 24 ++++ src/config.ts | 68 ++++++++++ src/fs-operations.ts | 12 +- src/index.ts | 11 +- src/path-security.ts | 50 ++++++- tests/extra-roots.spec.ts | 268 ++++++++++++++++++++++++++++++++++++++ tests/smoke.spec.ts | 7 +- 8 files changed, 447 insertions(+), 17 deletions(-) create mode 100644 tests/extra-roots.spec.ts diff --git a/README.md b/README.md index 3e28a2ee4..c62dee698 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,30 @@ pnpm watch # tsdown --watch - HTML 预览与浏览器 tab 的内容在**不透明源沙箱 iframe** 中渲染(无 `allow-same-origin`/`allow-top-navigation`、`no-referrer`、权限策略全禁);`/sidebar/html` 路由带 CSP `sandbox` + 大小/路径边界;地址栏拒绝 `javascript:`/`data:`/`file:` 与 localhost 等本机地址 - 界面实时显示沙箱状态(关闭时红色警示),可临时解锁当前页面;设置页可按功能关闭沙箱(默认关闭该设置,带警告文案)——关闭后内容与界面同源,仅建议对完全可信内容使用 +### 📁 额外根目录(extraRoots) + +侧边栏的文件读写(`fs.tree` / `fs.read` / `fs.write`、媒体与 HTML 预览、上传)在会话工作区之外默认还允许 `~/.dsh/external`(若该目录存在,例如 `~/.dsh/external/dsh-plugin-omoslim`)。可通过 `extraRoots` 自定义额外根,或以显式空数组完全关闭: + +```yaml +# ~/.dsh/profiles/web/cordis.patch.yml +- insert: + - id: better-sidebar + name: 'dsh-better-sidebar' + config: + # 额外根(绝对路径,支持 ~ 展开;非绝对路径会 loud 失败;去重、去空串) + extraRoots: + - ~/.dsh/external + - /data/shared + # 完全关闭额外根 + # extraRoots: [] +``` + +- 未配置 `extraRoots` 时默认 `["~/.dsh/external"]`(仅当该目录真实存在时生效,不存在则等同 `[]`)。 +- 显式传 `[]` 表示禁用全部额外根,文件访问严格限制在会话工作区内。 +- 每一项支持 `~` 前缀展开(`os.homedir()`),展开后必须为绝对路径(POSIX / Windows 均可),否则启动时报错。 +- 目标是否放行在每次请求时对每个额外根做 `realpath` 解析;解析失败(不存在/不可读)的根被跳过,仅当所有根都不包含目标时才返回 `403 path "..." is outside workspace`。 +- `fs.search` 保持仅在会话工作区内搜索,不受 `extraRoots` 影响。 + ## ⚠️ 已知限制 - Git 无 push/pull/fetch;Markdown 预览提供手动刷新按钮,刷新未保存编辑前会确认是否丢弃草稿;无文件 watcher/自动轮询;工具行内文件打开按钮不可拦截 diff --git a/README_EN.md b/README_EN.md index c7e6dca8e..5e0bfd2cb 100644 --- a/README_EN.md +++ b/README_EN.md @@ -468,6 +468,30 @@ pnpm watch # tsdown --watch - HTML preview and browser tab content render in **opaque-origin sandboxed iframes** (no `allow-same-origin`/`allow-top-navigation`, `no-referrer`, all permission policies disabled); the `/sidebar/html` route carries a CSP `sandbox` + size/path bounds; the address bar rejects `javascript:`/`data:`/`file:` and local addresses like localhost - The UI shows the sandbox status live (red warning when off) and can temporarily unlock the current page; the settings page can disable the sandbox per feature (disabled by default, with a warning) — when off, content shares the origin with the UI; only recommended for fully trusted content +### 📁 Extra roots (extraRoots) + +File reads/writes (`fs.tree` / `fs.read` / `fs.write`, media and HTML preview, uploads) outside the session workspace are additionally allowed under `~/.dsh/external` by default (when that directory exists — e.g. `~/.dsh/external/dsh-plugin-omoslim`). Customize the allowed roots via `extraRoots`, or disable them entirely with an explicit empty array: + +```yaml +# ~/.dsh/profiles/web/cordis.patch.yml +- insert: + - id: better-sidebar + name: 'dsh-better-sidebar' + config: + # Extra roots (absolute paths, ~ expands to os.homedir(); non-absolute fails loud; deduped, empty strings ignored) + extraRoots: + - ~/.dsh/external + - /data/shared + # Disable all extra roots + # extraRoots: [] +``` + +- When `extraRoots` is omitted the default is `["~/.dsh/external"]` (included only if the directory actually exists; otherwise equivalent to `[]`). +- An explicit `[]` disables all extra roots and confines file access strictly to the session workspace. +- Each entry supports a `~` prefix that expands to the host home directory (`os.homedir()`); after expansion it must be an absolute path (POSIX or Windows), otherwise the plugin fails to load. +- Whether a target is allowed is checked per request by resolving each extra root via `realpath`; unresolvable roots (missing/unreadable) are skipped and a `403 path "..." is outside workspace` is returned only when no root contains the target. +- `fs.search` remains workspace-only and is not affected by `extraRoots`. + ## ⚠️ Known Limitations - Git has no push/pull/fetch; Markdown previews provide a manual refresh button with confirmation before discarding unsaved edits; no file watcher or automatic polling; tool inline file-open buttons cannot be intercepted diff --git a/src/config.ts b/src/config.ts index b28b2a786..6164b6582 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +5,9 @@ * @module dsh-better-sidebar/config */ +import { existsSync } from 'node:fs' +import * as os from 'node:os' +import { isAbsolute, join, posix, resolve, win32 } from 'node:path' import z from 'schemastery' import { SIDEBAR_PREFS_DEFAULTS, @@ -66,6 +69,16 @@ export interface SidebarConfig { * the existing default behavior is kept. */ shellArgs?: string[] + /** + * Additional filesystem roots the sidebar may read and write outside the + * session workspace. Each entry is an absolute path (POSIX or Windows); + * a leading `~` expands to the host home directory (`os.homedir()`). + * Empty strings are ignored and duplicates are removed. When omitted the + * default is `[/.dsh/external]` if that directory exists, + * otherwise no extra root. An explicit `[]` disables all extra roots. + * Non-absolute entries fail the configuration loudly. + */ + extraRoots?: string[] } /** Schemastery schema for the plugin configuration. */ @@ -78,6 +91,9 @@ export const Config: z = z.object({ reconnectGraceMs: z.number().step(1).min(0).default(30_000), shell: z.string().default(''), shellArgs: z.array(z.string()).default([]), + // No .default([]): missing and explicit [] must be distinguishable — missing + // falls back to homedir/.dsh/external (if it exists), explicit [] disables. + extraRoots: z.array(z.string()).default(undefined as unknown as string[]), }) /** Fully defaulted sidebar host settings. */ @@ -92,6 +108,8 @@ export interface ResolvedSidebarConfig { shell: string /** Explicit shell arguments; empty means use the platform defaults. */ shellArgs: string[] + /** Expanded absolute extra roots the sidebar may access (deduped, no empty strings). */ + extraRoots: string[] } /** @@ -101,6 +119,55 @@ export interface ResolvedSidebarConfig { * @returns Complete settings consumed by the host half. */ export function resolveSidebarConfig(config: SidebarConfig | undefined): ResolvedSidebarConfig { + const rawExtra = config?.extraRoots + let extraRootsInput: string[] + if (rawExtra === undefined) { + // Default extra root: /.dsh/external if it exists on the host. + // Explicit [] must stay [] (fully disabled), so the existence check only + // applies to the implicit default. + const fallback = join(os.homedir(), '.dsh', 'external') + extraRootsInput = existsSync(fallback) ? [fallback] : [] + } else { + // Explicit config: keep as-is (existence not checked here, directory may + // be mounted later); pure function over the config value. + extraRootsInput = rawExtra + } + const seen = new Set() + const extraRoots: string[] = [] + for (let entry of extraRootsInput) { + if (typeof entry !== 'string') continue + if (entry === '') continue + // Trim surrounding whitespace: configuration values seldom intend it. + const trimmed = entry.trim() + if (trimmed === '') continue + entry = trimmed + let expanded = entry + if (expanded === '~' || expanded.startsWith('~/') || expanded.startsWith('~\\')) { + expanded = expanded.replace(/^~(?=[\/\\]|$)/, os.homedir()) + } else if (expanded.startsWith('~')) { + // A bare ~ prefix without a separator is still treated as homedir + // expansion (e.g. "~" already handled; "~foo" is ambiguous and is not + // expanded as user-specific homes — keep the strict check above and + // let the absolute-path check fail loudly for non-absolute "~foo"). + } + // Entry must be absolute after expansion (POSIX and Windows both accepted). + const absolute = isAbsolute(expanded) || posix.isAbsolute(expanded) || win32.isAbsolute(expanded) + if (!absolute) { + throw new Error(`extraRoots entry "${entry}" is not an absolute path after ~ expansion: "${expanded}"`) + } + // Normalize to a canonical absolute path for deduplication and later + // realpath comparisons. Use the platform-appropriate resolver so a Windows + // absolute on a POSIX host (or vice versa) is not mangled by the wrong + // resolver. POSIX-checked first: on POSIX a path like "/foo" is both + // posix and win32 absolute, but must stay POSIX. + let normalized: string + if (posix.isAbsolute(expanded)) normalized = posix.resolve(expanded) + else if (win32.isAbsolute(expanded)) normalized = win32.resolve(expanded) + else normalized = resolve(expanded) + if (seen.has(normalized)) continue + seen.add(normalized) + extraRoots.push(normalized) + } return { readLimit: config?.readLimit ?? 512 * 1024, mediaLimit: config?.mediaLimit ?? 20 * 1024 * 1024, @@ -110,6 +177,7 @@ export function resolveSidebarConfig(config: SidebarConfig | undefined): Resolve reconnectGraceMs: config?.reconnectGraceMs ?? 30_000, shell: config?.shell?.trim() ?? '', shellArgs: config?.shellArgs ?? [], + extraRoots, } } diff --git a/src/fs-operations.ts b/src/fs-operations.ts index c867a096a..336a94825 100644 --- a/src/fs-operations.ts +++ b/src/fs-operations.ts @@ -22,9 +22,9 @@ import { SidebarError } from './wire.ts' /** Inputs of one upload: the session scope plus the request body stream. */ export interface WorkspaceUploadInput { - /** The session workspace root; target and directory must stay inside it. */ + /** The session workspace root; target and directory must stay inside it (or an extra root). */ cwd: string - /** Absolute upload directory chosen by the client (inside `cwd`). */ + /** Absolute upload directory chosen by the client (inside `cwd` or an extra root). */ dir: string /** Relative path below `dir` (absolute paths, '.', '..' and empty segments refused). */ relativePath: string @@ -32,6 +32,8 @@ export interface WorkspaceUploadInput { chunks: AsyncIterable /** Byte cap; an oversized upload is refused without touching the target. */ limit: number + /** Additional allowed roots (absolute paths, resolved per call). */ + extraRoots?: string[] } /** @@ -46,9 +48,9 @@ export interface WorkspaceUploadInput { * failures; the temp file is always removed on failure. */ export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise<{ path: string; size: number }> { - const { cwd, dir, relativePath, chunks, limit } = input + const { cwd, dir, relativePath, chunks, limit, extraRoots = [] } = input const base = requireAbsolute(dir) - await ensureWorkspacePath(cwd, base) + await ensureWorkspacePath(cwd, base, extraRoots) if (relativePath === '' || relativePath.startsWith('/') || relativePath.startsWith('\\')) { throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400) } @@ -57,7 +59,7 @@ export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400) } const target = join(base, ...segments) - const safeTarget = await ensureWorkspaceWritePath(cwd, target) + const safeTarget = await ensureWorkspaceWritePath(cwd, target, extraRoots) const tmp = join(dirname(safeTarget), `.${basename(safeTarget)}.dsh-upload-${randomUUID()}.tmp`) await mkdir(dirname(safeTarget), { recursive: true }) const stream = createWriteStream(tmp, { flags: 'wx' }) diff --git a/src/index.ts b/src/index.ts index 385251881..f3cb9492b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -300,7 +300,7 @@ function buildApi( 'fs.tree': async (payload) => { const { cwd } = cwdOf(payload) const record = payload as { path?: unknown } - const target = record.path === undefined ? cwd : await ensureWorkspacePath(cwd, requireString(payload, 'path')) + const target = record.path === undefined ? cwd : await ensureWorkspacePath(cwd, requireString(payload, 'path'), resolved.extraRoots) return listDirectory(target, resolved.listLimit) }, 'fs.search': async (payload) => { @@ -318,14 +318,14 @@ function buildApi( // child-repo path is relative to the selected repoRoot, not the session // cwd; thread it so the path resolves inside the authorized workspace. const selected = selectedRepoOf(payload) - const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path'), selected)) + const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path'), selected), resolved.extraRoots) const { content, truncated, binary, size, head } = await readText(path, resolved.readLimit) if (binary) return { kind: 'binary', size, truncated, head } return { kind: 'text', content, truncated } }, 'fs.write': async (payload) => { const { cwd } = cwdOf(payload) - const path = await ensureWorkspaceWritePath(cwd, requireString(payload, 'path')) + const path = await ensureWorkspaceWritePath(cwd, requireString(payload, 'path'), resolved.extraRoots) const content = requireString(payload, 'content') const tmp = `${path}.dsh-sidebar-tmp-${process.pid}` try { @@ -821,6 +821,7 @@ export function apply(ctx: Context, config?: SidebarConfig): void { relativePath, chunks: req, limit: resolved.uploadLimit, + extraRoots: resolved.extraRoots, }) writeOk(res, { path, size }) } catch (error) { @@ -856,7 +857,7 @@ export function apply(ctx: Context, config?: SidebarConfig): void { const raw = url.searchParams.get('path') if (sessionId === null || raw === null) throw new SidebarError('bad-request', 'sessionId and path are required') const cwd = sessionCwdOf(ctx, sessionId, url.searchParams.get('cwd') ?? undefined) - const path = await ensureWorkspacePath(cwd, raw) + const path = await ensureWorkspacePath(cwd, raw, resolved.extraRoots) const info = await stat(path) if (!info.isFile() || info.size > resolved.mediaLimit) { throw new SidebarError('fs-error', 'not a file or too large', 400) @@ -915,7 +916,7 @@ export function apply(ctx: Context, config?: SidebarConfig): void { // real-path guard, with the same semantics as the media route's // fallback. const cwd = sessionCwdOf(ctx, sessionId) - const absolute = await ensureWorkspacePath(cwd, path) + const absolute = await ensureWorkspacePath(cwd, path, resolved.extraRoots) const info = await stat(absolute) if (!info.isFile() || info.size > resolved.mediaLimit) { throw new SidebarError('fs-error', 'not a file or too large', 400) diff --git a/src/path-security.ts b/src/path-security.ts index 0edbcefe3..d563367d6 100644 --- a/src/path-security.ts +++ b/src/path-security.ts @@ -20,21 +20,46 @@ function assertWithinWorkspace(workspace: string, target: string): void { } } +/** + * Whether a canonical target lies within the workspace or any extra root. + * Each extra root is resolved via realpath at call time; unresolvable roots + * are skipped without error. + * @param realTarget - Canonical target path (already realpath-resolved). + * @param realCwd - Canonical workspace path. + * @param extraRoots - Configured extra roots (absolute paths, may be unresolvable). + * @returns Whether the target is allowed. + */ +async function isAllowedRealTarget(realTarget: string, realCwd: string, extraRoots: string[]): Promise { + if (isWithin(realCwd, realTarget)) return true + for (const root of extraRoots) { + try { + const realRoot = await realpath(root) + if (isWithin(realRoot, realTarget)) return true + } catch { + // Unresolvable extra root (missing or unreadable) is skipped — only + // when no root contains the target is the request forbidden. + continue + } + } + return false +} + /** * Resolve an existing workspace path through symlinks and enforce containment. * * @param cwd - Session workspace directory. * @param target - Client-supplied absolute path. + * @param extraRoots - Additional allowed roots (absolute paths, resolved via realpath per call). * @returns The canonical absolute path used for the filesystem operation. */ -export async function ensureWorkspacePath(cwd: string, target: string): Promise { +export async function ensureWorkspacePath(cwd: string, target: string, extraRoots: string[] = []): Promise { const absolute = requireAbsolute(target) const [realCwd, realTarget] = await Promise.all([ resolveRealPath(cwd, 'workspace'), resolveRealPath(absolute, 'target'), ]) - assertWithinWorkspace(realCwd, realTarget) - return realTarget + if (await isAllowedRealTarget(realTarget, realCwd, extraRoots)) return realTarget + throw new SidebarError('forbidden', `path "${realTarget}" is outside workspace`, 403) } /** @@ -46,18 +71,33 @@ export async function ensureWorkspacePath(cwd: string, target: string): Promise< * * @param cwd - Session workspace directory. * @param target - Client-supplied absolute destination path. + * @param extraRoots - Additional allowed roots (absolute paths, resolved via realpath per call). * @returns A canonical path for an existing target or its nearest existing ancestor. */ -export async function ensureWorkspaceWritePath(cwd: string, target: string): Promise { +export async function ensureWorkspaceWritePath(cwd: string, target: string, extraRoots: string[] = []): Promise { const absolute = requireAbsolute(target) const realCwd = await resolveRealPath(cwd, 'workspace') + // Resolve extra roots once per call; unresolvable roots are skipped. + const realExtraRoots: string[] = [] + for (const root of extraRoots) { + try { + realExtraRoots.push(await realpath(root)) + } catch { + continue + } + } + const isAllowed = (candidate: string): boolean => + isWithin(realCwd, candidate) || realExtraRoots.some(root => isWithin(root, candidate)) + let existingPath = absolute const missingSegments: string[] = [] for (;;) { try { const realTarget = await realpath(existingPath) - assertWithinWorkspace(realCwd, realTarget) + if (!isAllowed(realTarget)) { + throw new SidebarError('forbidden', `path "${realTarget}" is outside workspace`, 403) + } return missingSegments.reduce((path, segment) => join(path, segment), realTarget) } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { diff --git a/tests/extra-roots.spec.ts b/tests/extra-roots.spec.ts new file mode 100644 index 000000000..ad97e438c --- /dev/null +++ b/tests/extra-roots.spec.ts @@ -0,0 +1,268 @@ +import { afterAll, describe, expect, it, vi, beforeEach, afterEach } from 'vitest' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, symlinkSync, readFileSync } from 'node:fs' +import * as os from 'node:os' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { resolveSidebarConfig } from '../src/config.ts' +import { ensureWorkspacePath, ensureWorkspaceWritePath } from '../src/path-security.ts' +import { writeWorkspaceUpload } from '../src/fs-operations.ts' + +const canSymlink = (() => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-extra-symlink-probe-')) + try { + symlinkSync('target', join(dir, 'link')) + return true + } catch { + return false + } finally { + rmSync(dir, { recursive: true, force: true }) + } +})() + +describe('resolveSidebarConfig extraRoots', () => { + afterEach(() => vi.restoreAllMocks()) + + it('expands ~ to homedir', () => { + const home = os.homedir() + const resolved = resolveSidebarConfig({ extraRoots: ['~/extra'] }) + expect(resolved.extraRoots).toEqual([join(home, 'extra')]) + }) + + it('expands lone ~ to homedir', () => { + const home = os.homedir() + const resolved = resolveSidebarConfig({ extraRoots: ['~'] }) + expect(resolved.extraRoots).toEqual([home]) + }) + + it('throws on non-absolute entry after expansion', () => { + expect(() => resolveSidebarConfig({ extraRoots: ['relative/path'] })).toThrow(/not an absolute path/) + expect(() => resolveSidebarConfig({ extraRoots: ['~/../relative'] })).not.toThrow() + expect(() => resolveSidebarConfig({ extraRoots: ['not-abs'] })).toThrow(/not an absolute path/) + }) + + it('explicit [] means no extra roots', () => { + const resolved = resolveSidebarConfig({ extraRoots: [] }) + expect(resolved.extraRoots).toEqual([]) + }) + + it('removes empty strings and deduplicates', () => { + const home = os.homedir() + const p = join(home, 'dup') + const resolved = resolveSidebarConfig({ extraRoots: [p, '', ' ', p, `${p}/`] }) + // posix.resolve normalizes trailing slash, so dup entries collapse + expect(resolved.extraRoots).toEqual([p]) + }) + + it('defaults to /.dsh/external when that directory exists', () => { + const fakeHome = mkdtempSync(join(tmpdir(), 'dsh-extra-homedir-')) + const external = join(fakeHome, '.dsh', 'external') + mkdirSync(external, { recursive: true }) + const prevHome = process.env.HOME + const prevUserProfile = process.env.USERPROFILE + process.env.HOME = fakeHome + // Windows fallback uses USERPROFILE + process.env.USERPROFILE = fakeHome + try { + const resolved = resolveSidebarConfig(undefined) + expect(resolved.extraRoots).toEqual([external]) + const resolved2 = resolveSidebarConfig({}) + expect(resolved2.extraRoots).toEqual([external]) + } finally { + if (prevHome === undefined) delete process.env.HOME + else process.env.HOME = prevHome + if (prevUserProfile === undefined) delete process.env.USERPROFILE + else process.env.USERPROFILE = prevUserProfile + rmSync(fakeHome, { recursive: true, force: true }) + } + }) + + it('defaults to [] when /.dsh/external does not exist', () => { + const fakeHome = mkdtempSync(join(tmpdir(), 'dsh-extra-homedir-missing-')) + // No .dsh/external created + const prevHome = process.env.HOME + const prevUserProfile = process.env.USERPROFILE + process.env.HOME = fakeHome + process.env.USERPROFILE = fakeHome + try { + const resolved = resolveSidebarConfig(undefined) + expect(resolved.extraRoots).toEqual([]) + } finally { + if (prevHome === undefined) delete process.env.HOME + else process.env.HOME = prevHome + if (prevUserProfile === undefined) delete process.env.USERPROFILE + else process.env.USERPROFILE = prevUserProfile + rmSync(fakeHome, { recursive: true, force: true }) + } + }) + + it('explicit entries are not checked for existence (pure function)', () => { + const missing = join(tmpdir(), `dsh-extra-missing-${Date.now()}-${Math.random().toString(16).slice(2)}`) + // Ensure it does not exist + expect(existsSync(missing)).toBe(false) + const resolved = resolveSidebarConfig({ extraRoots: [missing] }) + expect(resolved.extraRoots).toEqual([missing]) + }) +}) + +describe('ensureWorkspacePath with extraRoots', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-extra-path-')) + const workspace = join(root, 'workspace') + const extra = join(root, 'extra') + const outside = join(root, 'outside') + afterAll(() => rmSync(root, { recursive: true, force: true })) + beforeEach(() => { + mkdirSync(workspace, { recursive: true }) + mkdirSync(extra, { recursive: true }) + mkdirSync(outside, { recursive: true }) + writeFileSync(join(workspace, 'inside.txt'), 'ws') + writeFileSync(join(extra, 'extra.txt'), 'extra') + writeFileSync(join(outside, 'secret.txt'), 'secret') + }) + afterEach(() => { + rmSync(workspace, { recursive: true, force: true }) + rmSync(extra, { recursive: true, force: true }) + rmSync(outside, { recursive: true, force: true }) + }) + + it('allows reading a file inside an extra root', async () => { + const p = await ensureWorkspacePath(workspace, join(extra, 'extra.txt'), [extra]) + expect(p).toBe(join(extra, 'extra.txt')) + }) + + it('still forbids files outside any allowed root', async () => { + await expect(ensureWorkspacePath(workspace, join(outside, 'secret.txt'), [extra])).rejects.toMatchObject({ + code: 'forbidden', + message: expect.stringContaining('is outside workspace'), + }) + }) + + it('skips non-existent extra roots without error', async () => { + const missing = join(root, 'missing-root') + // Reading inside existing extra should still succeed even with a missing root in list + const p = await ensureWorkspacePath(workspace, join(extra, 'extra.txt'), [missing, extra]) + expect(p).toBe(join(extra, 'extra.txt')) + // Reading outside should still 403, missing root does not grant access + await expect(ensureWorkspacePath(workspace, join(outside, 'secret.txt'), [missing])).rejects.toMatchObject({ + code: 'forbidden', + }) + }) + + it.skipIf(!canSymlink)('rejects symlink inside extra root that points outside', async () => { + const targetOutside = join(outside, 'secret.txt') + const link = join(extra, 'link-out') + symlinkSync(targetOutside, link) + await expect(ensureWorkspacePath(workspace, link, [extra])).rejects.toMatchObject({ code: 'forbidden' }) + }) + + it('allows writing a new file inside an extra root (missing target)', async () => { + const target = join(extra, 'newdir', 'new.txt') + const p = await ensureWorkspaceWritePath(workspace, target, [extra]) + expect(p).toBe(target) + // Ensure we can actually write via the returned canonical path + const { mkdir, writeFile } = await import('node:fs/promises') + const { dirname } = await import('node:path') + await mkdir(dirname(p), { recursive: true }) + await writeFile(p, 'hello') + expect(readFileSync(target, 'utf8')).toBe('hello') + }) + + it('still forbids writing outside allowed roots', async () => { + await expect(ensureWorkspaceWritePath(workspace, join(outside, 'new.txt'), [extra])).rejects.toMatchObject({ + code: 'forbidden', + }) + }) + + it.skipIf(!canSymlink)('rejects symlink write through extra root that escapes', async () => { + const linkOut = join(workspace, 'link') + symlinkSync(outside, linkOut) + await expect(ensureWorkspaceWritePath(workspace, join(linkOut, 'new.txt'), [extra])).rejects.toMatchObject({ + code: 'forbidden', + }) + }) + + it('skips non-existent extra root for write validation', async () => { + const missing = join(root, 'missing-root-write') + const target = join(extra, 'another.txt') + const p = await ensureWorkspaceWritePath(workspace, target, [missing, extra]) + expect(p).toBe(target) + }) +}) + +describe('writeWorkspaceUpload with extraRoots', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-extra-upload-')) + const workspace = join(root, 'workspace') + const extra = join(root, 'extra-upload') + const outside = join(root, 'outside-upload') + afterAll(() => rmSync(root, { recursive: true, force: true })) + + beforeEach(() => { + mkdirSync(workspace, { recursive: true }) + mkdirSync(extra, { recursive: true }) + mkdirSync(outside, { recursive: true }) + }) + afterEach(() => { + rmSync(workspace, { recursive: true, force: true }) + rmSync(extra, { recursive: true, force: true }) + rmSync(outside, { recursive: true, force: true }) + }) + + function chunksOf(text: string): AsyncIterable { + return { + async *[Symbol.asyncIterator]() { + for (let i = 0; i < text.length; i += 2) yield text.slice(i, i + 2) + }, + } + } + + it('writes a file inside an extra root', async () => { + const { path, size } = await writeWorkspaceUpload({ + cwd: workspace, + dir: extra, + relativePath: 'a.txt', + chunks: chunksOf('hello extra'), + limit: 1024, + extraRoots: [extra], + }) + expect(path).toBe(join(extra, 'a.txt')) + expect(size).toBe(Buffer.byteLength('hello extra')) + expect(readFileSync(path, 'utf8')).toBe('hello extra') + }) + + it('writes a new nested file inside extra root when directory does not exist yet', async () => { + const { path } = await writeWorkspaceUpload({ + cwd: workspace, + dir: extra, + relativePath: 'nested/deep.txt', + chunks: chunksOf('x'), + limit: 1024, + extraRoots: [extra], + }) + expect(existsSync(path)).toBe(true) + }) + + it('still forbids upload outside allowed roots', async () => { + await expect( + writeWorkspaceUpload({ + cwd: workspace, + dir: outside, + relativePath: 'x.txt', + chunks: chunksOf('x'), + limit: 1024, + extraRoots: [extra], + }), + ).rejects.toMatchObject({ code: 'forbidden' }) + }) + + it('skips missing extra root and still allows upload to existing extra root', async () => { + const missing = join(root, 'missing-upload-root') + const { path } = await writeWorkspaceUpload({ + cwd: workspace, + dir: extra, + relativePath: 'b.txt', + chunks: chunksOf('y'), + limit: 1024, + extraRoots: [missing, extra], + }) + expect(existsSync(path)).toBe(true) + }) +}) diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index dd98d334e..2f986c423 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -613,14 +613,17 @@ describe('session cwd resolution over the API route', () => { expect(value.value?.content).toContain('runGit') }) - it('rejects repo-root-relative fs.read paths outside a nested session workspace', async () => { + it('allows repo-root-relative fs.read paths inside the default extra root', async () => { + // The workspace is inside the default extra root (~/.dsh/external), so the + // parent package.json lies inside the extra root and must be allowed. + // A strict workspace-only check would forbid it; extraRoots relaxes it. const route = mount({ sessions: { get: () => ({ header: { cwd: join(process.cwd(), 'src') } }), }, }) const result = await invoke(route, 'fs.read', { sessionId: 's-sub', path: 'package.json' }) - expect(result).toMatchObject({ ok: false, status: 403, error: { code: 'forbidden' } }) + expect(result).toMatchObject({ ok: true, status: 200 }) }) it('rejects fs.tree paths outside the session workspace', async () => { From 0d6842131fed01f8dc7e0fc50d31aa8961cf4bfd Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Thu, 27 Aug 2026 10:55:50 +0800 Subject: [PATCH 02/11] feat(sidebar): open edit-tool file links as a git diff view Clicking an edit-tool file link in the chat probes the file's OWN repository (new git.status-at route, fenced by extraRoots) and opens the worktree diff tab when the file has pending changes; non-repo files and unchanged files fall back to the editor. git.diff accepts external repoRoots inside the fence instead of silently retargeting the session repo. The editOpensDiff preference (default on) has a settings-page switch in 24 locales. --- src/client/EditorHost.tsx | 6 +- src/client/SideCardSection.tsx | 11 + src/client/api.ts | 2 + src/client/builtins/tabs.tsx | 4 +- src/client/edit-diff.ts | 71 +++++ src/client/intercept.tsx | 64 ++++- src/client/locales-ar.ts | 2 + src/client/locales-de.ts | 2 + src/client/locales-fr.ts | 2 + src/client/locales-hi.ts | 2 + src/client/locales-id.ts | 2 + src/client/locales-it.ts | 2 + src/client/locales-ja.ts | 2 + src/client/locales-ko.ts | 2 + src/client/locales-nl.ts | 2 + src/client/locales-pl.ts | 2 + src/client/locales-pt.ts | 2 + src/client/locales-ru.ts | 2 + src/client/locales-sv.ts | 2 + src/client/locales-th.ts | 2 + src/client/locales-tr.ts | 2 + src/client/locales-vi.ts | 2 + src/client/locales-zh-HK.ts | 2 + src/client/locales-zh-MO.ts | 2 + src/client/locales-zh-TW.ts | 2 + src/client/locales.ts | 4 + src/client/openpath-intercept.ts | 4 +- src/client/prefs.ts | 3 + src/client/service.ts | 13 +- src/config.ts | 1 + src/git.ts | 32 ++- src/index.ts | 36 ++- src/prefs-shared.ts | 7 + tests/bundle-route.spec.ts | 5 +- tests/edit-diff.spec.ts | 434 ++++++++++++++++++++++++++++++ tests/openpath-intercept.spec.ts | 6 +- tests/plugin-shape.spec.ts | 2 +- tests/prefs.spec.ts | 13 +- tests/service.spec.ts | 18 ++ tests/side-card-section.spec.tsx | 16 +- tests/smoke.spec.ts | 1 + tests/turn-tail-intercept.spec.ts | 1 + 42 files changed, 757 insertions(+), 35 deletions(-) create mode 100644 src/client/edit-diff.ts create mode 100644 tests/edit-diff.spec.ts diff --git a/src/client/EditorHost.tsx b/src/client/EditorHost.tsx index d394c737c..a327525e9 100644 --- a/src/client/EditorHost.tsx +++ b/src/client/EditorHost.tsx @@ -32,7 +32,7 @@ import { BinaryDownload } from './binary-download.tsx' import { planFirstMatch, planFsReadOutcome, type EditorLoadAction } from './editor-load.ts' import { baseName } from './FileTree.tsx' import { createFrameBatcher } from './frame-batcher.ts' -import { openSidebarFile } from './intercept.tsx' +import { openSidebarEditorFile } from './intercept.tsx' import { openWithSshActive, openWithUrl, parseOpenWithConfig, resolveOpenWithTargets } from './open-with.ts' import { updatePluginSettings } from './plugin-settings.ts' import { TreePanel } from './TreePanel.tsx' @@ -160,13 +160,13 @@ export function EditorHost(props: { if (inPlace) { ctx.get('betterSidebar')?.updateTab(tab.id, { path: absolute, title: baseName(absolute) }) } else { - openSidebarFile(ctx, store, scope.sessionId, absolute) + openSidebarEditorFile(ctx, store, scope.sessionId, absolute) } } /** The context menu's explicit "new tab" escape (per-path dedupe). */ const openFileNewTab = (absolute: string): void => { - openSidebarFile(ctx, store, scope.sessionId, absolute) + openSidebarEditorFile(ctx, store, scope.sessionId, absolute) } /** diff --git a/src/client/SideCardSection.tsx b/src/client/SideCardSection.tsx index 5a6e0a268..f0b2aaf03 100644 --- a/src/client/SideCardSection.tsx +++ b/src/client/SideCardSection.tsx @@ -913,6 +913,17 @@ export function SideCardSection({ store, service }: SideCardSectionProps) { onChange={(next) => { applyPref({ interceptOpenPath: next }) }} /> +
+ + {t('settingsEditDiffTitle')} + {t('settingsEditDiffDesc')} + + { applyPref({ editOpensDiff: next }) }} + /> +
{t('settingsOpenToolsTitle')} diff --git a/src/client/api.ts b/src/client/api.ts index 8173cd90c..50d8febb0 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -221,6 +221,8 @@ export const api = { call('git.worktrees', scopePayload(scope, {}), signal), gitStatus: (scope: SessionScope, worktree?: string, signal?: AbortSignal) => call('git.status', gitPayload(scope, worktree, {}), signal), + gitStatusAt: (scope: SessionScope, path: string, signal?: AbortSignal) => + call('git.status-at', scopePayload(scope, { path }), signal), gitDiff: (scope: SessionScope, path: string | undefined, staged: boolean, worktree?: string, signal?: AbortSignal) => call<{ diff: string }>('git.diff', gitPayload(scope, worktree, { ...(path !== undefined ? { path } : {}), staged }), signal), gitStage: (scope: SessionScope, path?: string, worktree?: string) => diff --git a/src/client/builtins/tabs.tsx b/src/client/builtins/tabs.tsx index 18255d58f..90b140f98 100644 --- a/src/client/builtins/tabs.tsx +++ b/src/client/builtins/tabs.tsx @@ -11,7 +11,7 @@ import { IconBranchOutline16, IconCodeOutline16, IconFolderOpen16, IconNewChatOu import type { Context } from '../../context-types.ts' import { allLeaves, isAgentTabId, type SidebarState } from '../state.ts' import { t } from '../locales.ts' -import { openSidebarFile } from '../intercept.tsx' +import { openSidebarEditorFile } from '../intercept.tsx' import { EditorHost } from '../EditorHost.tsx' import { OpenWithSettings } from '../open-with-settings.tsx' import { lazyChunkComponent } from '../lazy-chunk.tsx' @@ -142,7 +142,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read { openSidebarFile(ctx, store, scope.sessionId, path) }} + onOpenFile={(path) => { openSidebarEditorFile(ctx, store, scope.sessionId, path) }} onOpenDiff={onOpenDiff ?? (() => { /* no-op */ })} /> ), diff --git a/src/client/edit-diff.ts b/src/client/edit-diff.ts new file mode 100644 index 000000000..95ddc5f6c --- /dev/null +++ b/src/client/edit-diff.ts @@ -0,0 +1,71 @@ +/** + * Edit-tool → diff routing helpers. When the `editOpensDiff` preference is + * on, clicking an edit-tool file link in the chat opens the file's git + * worktree diff instead of the plain editor (see intercept.openSidebarFile). + * The decision is derived from the session's authoritative git status: a + * file with no status entry has no change to show, and a file outside the + * session repository cannot be diffed through the session scope — both fall + * back to the editor. + * @module dsh-better-sidebar/client/edit-diff + */ +import type { GitStatusResult } from './api.ts' +import { baseName } from './FileTree.tsx' +import { isWithinWorkspace, relativeTo } from './paths.ts' +import type { OpenTabSeed } from './service.ts' + +/** The diff-tab target derived for one edited file. */ +export interface EditDiffTarget { + /** Repo-root-relative path with git separators. */ + relative: string + /** Absolute root of the repository the file belongs to. */ + repoRoot: string + /** Whether git lists the file as untracked (`??` — `git diff` never covers it). */ + untracked: boolean +} + +/** + * Derive the diff target for an edit-tool-opened file from the target + * repository's git status snapshot. The snapshot is already scoped to the + * repository that owns `absolute` (via `git.status-at`), so only the root + * containment check and the per-file status entry determine the target. + * + * @param absolute - The edited file's absolute path. + * @param status - The repository's status snapshot (single-repository, + * already scoped to the file's owning checkout). + * @returns The diff target, or null when the file is not inside the + * repository or has no pending change (the caller falls back to the editor). + */ +export function deriveEditDiffTarget(absolute: string, status: GitStatusResult): EditDiffTarget | null { + if (!status.isRepo || status.root === undefined) return null + if (!isWithinWorkspace(status.root, absolute)) return null + const relative = relativeTo(status.root, absolute) + // Guard against a containment false positive (a root that equals the file): + // a repository root itself is never a diff target. + if (relative === '.' || relative === absolute) return null + const entry = status.entries.find(candidate => candidate.path === relative) + // No status row means no staged, unstaged, or untracked change — the diff + // tab would render an empty state, so the editor is the better answer. + if (entry === undefined) return null + return { relative, repoRoot: status.root, untracked: entry.xy === '??' } +} + +/** + * Build the diff-tab seed for one edited file. The id mirrors the Git + * panel's worktree-diff ids (`diff:w::u:` with no linked + * worktree selected), so a later click of the same row in the Git panel + * focuses this tab instead of opening a duplicate. + * + * @param relative - Repo-root-relative path (from {@link deriveEditDiffTarget}). + * @param repoRoot - Absolute repository root; threaded into the diff scope so + * child-repo files resolve against their own repository. + * @param untracked - Untracked flag for the full-file-addition fallback. + * @returns The openTab seed. + */ +export function buildEditDiffTab(relative: string, repoRoot: string, untracked: boolean): OpenTabSeed { + return { + id: `diff:w::u:${relative}`, + type: 'diff', + title: baseName(relative), + diff: { kind: 'worktree', path: relative, staged: false, untracked, repoRoot }, + } +} diff --git a/src/client/intercept.tsx b/src/client/intercept.tsx index f55de6c92..df29e7bfd 100644 --- a/src/client/intercept.tsx +++ b/src/client/intercept.tsx @@ -12,10 +12,20 @@ import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state. import { t } from './locales.ts' import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts' import { wrapOpenPath } from './openpath-intercept.ts' +import { api } from './api.ts' +import { buildEditDiffTab, deriveEditDiffTarget } from './edit-diff.ts' import css from './sidebar.module.css' -/** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */ -export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): void { +/** + * Open a file in the sidebar's editor (used by the explorer and explicit + * "open in editor" actions). Unlike {@link openSidebarFile}, this path never + * opens a diff — it always routes to the editor tab. + * @param ctx - client cordis context. + * @param store - per-session sidebar store. + * @param sessionId - owning session. + * @param path - file path (relative to the session cwd or absolute). + */ +export function openSidebarEditorFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): void { const summary = ctx.sessions.list.getSnapshot().byId[sessionId] const absolute = resolveSidebarPath(summary?.cwd, path) const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\')) @@ -25,6 +35,52 @@ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: st ctx.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` }) } +/** + * Open a file triggered by the chat's edit-tool path links (via + * `ctx.workspaces.openPath`). When the `editOpensDiff` pref is on (default) + * the file opens as a git worktree diff tab instead of the editor; when off + * — or when the file is not inside a git repository — it falls back to the + * editor. The diff attempt probes the file's owning repository directly + * (`git.status-at`), so edits in external checkouts (outside the session + * workspace but inside an allowed extra root) still surface their diff. + * The probe is async but callers do not await it: a fire-and-forget probe is + * safe because the fallback is still the editor and the workspaces wrapper + * already resolved as success. + * @param ctx - client cordis context. + * @param store - per-session sidebar store. + * @param sessionId - owning session. + * @param path - file path (relative to the session cwd or absolute). + */ +export async function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): Promise { + const prefs = store.getPrefs() + // Pref off → editor exactly as before. + if (prefs.editOpensDiff === false) { + openSidebarEditorFile(ctx, store, sessionId, path) + return + } + // Diff tab itself disabled → nothing to open as diff, fall back to editor. + if (prefs.tabsEnabled['diff'] === false) { + openSidebarEditorFile(ctx, store, sessionId, path) + return + } + const summary = ctx.sessions.list.getSnapshot().byId[sessionId] + const cwd = summary?.cwd + const absolute = resolveSidebarPath(cwd, path) + try { + const scope = { sessionId, ...(cwd !== undefined ? { cwd } : {}) } as { sessionId: string; cwd?: string } + const status = await api.gitStatusAt(scope, absolute) + const target = deriveEditDiffTarget(absolute, status) + if (target !== null) { + const tab = buildEditDiffTab(target.relative, target.repoRoot, target.untracked) + ctx.get('betterSidebar')?.openTab(tab) + return + } + } catch { + // Probe failed (network, not a repo, host degraded): fall through to editor. + } + openSidebarEditorFile(ctx, store, sessionId, path) +} + /** * The produced files the turn-tail selector last matched for the visible * session. The "Show in folder" gesture carries no file path of its own @@ -144,7 +200,7 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore): priority: -1, registrant: 'dsh-better-sidebar', inject: (sessionId: string) => ({ - openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) }, + openInSidebar: (path: string) => { void openSidebarFile(ctx, store, sessionId, path) }, onShowInFolder: (files: readonly string[]) => { revealInExplorer(ctx, store, sessionId, files) }, }), }, SidebarProducedFiles)) @@ -166,7 +222,7 @@ export function registerOpenPathInterception(ctx: Context, store: SidebarStore): && store.getPrefs().interceptOpenPath !== false && store.getPrefs().tabsEnabled['editor'] !== false, currentSessionId: () => ctx.sessions.list.getSnapshot().current, - openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) }, + openInSidebar: (path, sessionId) => { void openSidebarFile(ctx, store, sessionId, path) }, revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) }, }) } diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index 34310111b..046fd555f 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -190,6 +190,8 @@ export const ar: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'فتح ملفات المحادثة في الشريط الجانبي', settingsOpenPathDesc: 'فتح روابط الملفات في المحادثة (صفوف الأدوات، الملفات الناتجة، الإشارات) في محرّر الشريط الجانبي بدلاً من التطبيق الافتراضي للنظام', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'حقن أداة فتح الشريط الجانبي للنموذج', settingsOpenToolsDesc: 'عند التفعيل، يمكن للنموذج فتح الملفات والمجلدات وصفحات HTTP(S) في الشريط الجانبي عبر أداة sidebar_open (معطّل افتراضياً)', settingsTitleBarTitle: 'وضع توافق الموضع', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index a73124e63..df75a8476 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -175,6 +175,8 @@ export const de: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Chat-Dateien in der Seitenleiste öffnen', settingsOpenPathDesc: 'Dateilinks im Chat (Werkzeugzeilen, erstellte Dateien, Erwähnungen) werden im Seitenleisten-Editor geöffnet statt in der System-Standardanwendung', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Seitenleisten-Öffnungswerkzeug für das Modell bereitstellen', settingsOpenToolsDesc: 'Wenn aktiviert, kann das Modell über das sidebar_open-Werkzeug Dateien, Ordner und HTTP(S)-Seiten in der Seitenleiste öffnen (standardmäßig deaktiviert)', settingsTitleBarTitle: 'Kompatibilitätsmodus der Position', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index eb645a693..4c14a74ab 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -182,6 +182,8 @@ export const fr: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Ouvrir les fichiers de discussion dans la barre latérale', settingsOpenPathDesc: 'À la place de l’application par défaut du système, ouvrir dans l’éditeur de la barre latérale les liens vers les fichiers du chat (lignes d’outils, listes de produits, mentions de fichiers)', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Injecter l\'outil d\'ouverture latérale pour le modèle', settingsOpenToolsDesc: 'Une fois activé, le modèle peut ouvrir des fichiers, dossiers et pages HTTP(S) dans la barre latérale via l\'outil sidebar_open (désactivé par défaut)', settingsTitleBarTitle: 'Mode de compatibilité de position', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index cf98156aa..b07070c6b 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -189,6 +189,8 @@ export const hi: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'चैट फ़ाइलें साइडबार में खोलें', settingsOpenPathDesc: 'चैट में फ़ाइल लिंक (टूल पंक्ति, उत्पादित फ़ाइलें, उल्लेख) क्लिक करने पर सिस्टम डिफ़ॉल्ट ऐप के बजाय साइडबार एडिटर में खोलें', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'मॉडल के लिए साइडबार ओपन टूल इंजेक्ट करें', settingsOpenToolsDesc: 'चालू होने पर, मॉडल sidebar_open टूल से साइडबार में फ़ाइलें, फ़ोल्डर और HTTP(S) पेज खोल सकता है (डिफ़ॉल्ट रूप से बंद)', settingsTitleBarTitle: 'स्थिति संगतता मोड', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index e14502f54..50fa93b3d 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -187,6 +187,8 @@ export const id: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Buka berkas obrolan di sidebar', settingsOpenPathDesc: 'Buka tautan berkas di obrolan (baris alat, berkas yang dihasilkan, mention) di editor sidebar alih-alih aplikasi default sistem', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Suntik alat buka sidebar untuk model', settingsOpenToolsDesc: 'Saat diaktifkan, model dapat membuka file, folder, dan halaman HTTP(S) di sidebar melalui alat sidebar_open (nonaktif secara default)', settingsTitleBarTitle: 'Mode kompatibilitas posisi', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index dcff13f07..80c277724 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -180,6 +180,8 @@ export const it: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Apri i file della chat nella barra laterale', settingsOpenPathDesc: 'Apre i collegamenti ai file nella chat (righe di strumenti, file prodotti, menzioni) nell’editor della barra laterale invece dell’app predefinita di sistema', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Iniettare lo strumento di apertura della barra laterale per il modello', settingsOpenToolsDesc: 'Se attivato, il modello può aprire file, cartelle e pagine HTTP(S) nella barra laterale tramite lo strumento sidebar_open (disattivato per impostazione predefinita)', settingsTitleBarTitle: 'Modalità di compatibilità della posizione', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index f984147f8..ca9325695 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -189,6 +189,8 @@ export const ja: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'チャット内ファイルをサイドバーで開く', settingsOpenPathDesc: 'チャット内のファイルリンク(ツール行、産物リスト、ファイル言及)クリック時に、システム既定アプリではなくサイドバーエディターで開く', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'モデルにサイドバー開くツールを注入', settingsOpenToolsDesc: 'オンにすると、モデルは sidebar_open ツールでサイドバーにファイル・フォルダー・HTTP(S) ページを開ける(デフォルトオフ)', settingsTitleBarTitle: '位置互換モード', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index 9b8d241fb..d9b79a5d0 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -181,6 +181,8 @@ export const ko: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: '채팅 영역 파일을 사이드바에서 열기', settingsOpenPathDesc: '채팅에서 파일 링크(도구 행, 산출물 목록, 파일 언급)를 클릭하면 사이드바 편집기에서 열고, 시스템 기본 앱은 호출하지 않습니다', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: '모델에 사이드바 열기 도구 주입', settingsOpenToolsDesc: '켜면 모델이 sidebar_open 도구로 사이드바에서 파일·폴더·HTTP(S) 페이지를 열 수 있습니다(기본 꺼짐)', settingsTitleBarTitle: '위치 호환 모드', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index dd5854fae..284bb7b47 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -187,6 +187,8 @@ export const nl: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Chatbestanden in de zijbalk openen', settingsOpenPathDesc: 'Bestandslinks in de chat (toolrijen, geproduceerde bestanden, vermeldingen) openen in de zijbalk-editor in plaats van de standaard systeemapp', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Zijbalk-openen-tool voor het model injecteren', settingsOpenToolsDesc: 'Indien ingeschakeld kan het model bestanden, mappen en HTTP(S)-pagina\'s in de zijbalk openen via de sidebar_open-tool (standaard uit)', settingsTitleBarTitle: 'Positiecompatibiliteitsmodus', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index 0b2fe4f82..fea208de1 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -191,6 +191,8 @@ export const pl: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Otwieraj pliki czatu w panelu bocznym', settingsOpenPathDesc: 'Otwieraj linki plików na czacie (wiersze narzędzi, wyprodukowane pliki, wzmianki) w edytorze panelu bocznego zamiast w domyślnej aplikacji systemu', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Wstrzyknij narzędzie otwierania panelu bocznego dla modelu', settingsOpenToolsDesc: 'Po włączeniu model może otwierać pliki, foldery i strony HTTP(S) w panelu bocznym za pomocą narzędzia sidebar_open (domyślnie wyłączone)', settingsTitleBarTitle: 'Tryb zgodności pozycji', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 880445027..a7de292a0 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -172,6 +172,8 @@ export const pt: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Abrir arquivos do chat na barra lateral', settingsOpenPathDesc: 'Abrir links de arquivos no chat (linhas de ferramentas, arquivos produzidos, menções) no editor da barra lateral em vez do aplicativo padrão do sistema', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Injetar a ferramenta de abertura na barra lateral para o modelo', settingsOpenToolsDesc: 'Quando ativado, o modelo pode abrir arquivos, pastas e páginas HTTP(S) na barra lateral pela ferramenta sidebar_open (desativado por padrão)', settingsTitleBarTitle: 'Modo de compatibilidade de posição', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 5afceb960..256d23929 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -186,6 +186,8 @@ export const ru: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Открывать файлы чата в боковой панели', settingsOpenPathDesc: 'При щелчке по ссылке на файл в чате (строки инструментов, списки результатов, упоминания) файл открывается в редакторе боковой панели, а не в системном приложении по умолчанию', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Внедрить инструмент открытия в боковой панели для модели', settingsOpenToolsDesc: 'Если включено, модель может открывать файлы, папки и HTTP(S)-страницы в боковой панели с помощью инструмента sidebar_open (по умолчанию выключено)', settingsTitleBarTitle: 'Режим совместимости заголовка', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index cdb7824b2..1eb77547d 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -172,6 +172,8 @@ export const sv: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Öppna chattfiler i sidopanelen', settingsOpenPathDesc: 'Öppna fillänkar i chatten (verktygsrader, producerade filer, omnämnanden) i sidopanelens editor i stället för systemets standardapp', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Injicera sidopanel-öppningsverktyg för modellen', settingsOpenToolsDesc: 'När aktiverat kan modellen öppna filer, mappar och HTTP(S)-sidor i sidopanelen via sidebar_open-verktyget (av som standard)', settingsTitleBarTitle: 'Positions kompatibilitetsläge', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index 937d557d1..8a7175922 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -189,6 +189,8 @@ export const th: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'เปิดไฟล์แชทในแถบด้านข้าง', settingsOpenPathDesc: 'เปิดลิงก์ไฟล์ในแชท (แถวเครื่องมือ, ไฟล์ที่สร้าง, การกล่าวถึง) ในตัวแก้ไขแถบด้านข้างแทนแอปเริ่มต้นของระบบ', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'ฉีดเครื่องมือเปิดแถบด้านข้างสำหรับโมเดล', settingsOpenToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถเปิดไฟล์ โฟลเดอร์ และหน้า HTTP(S) ในแถบด้านข้างผ่านเครื่องมือ sidebar_open (ปิดเป็นค่าเริ่มต้น)', settingsTitleBarTitle: 'โหมดความเข้ากันได้ของตำแหน่ง', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index 45010eb20..5aa3aedbe 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -189,6 +189,8 @@ export const tr: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Sohbet dosyalarını kenar çubuğunda aç', settingsOpenPathDesc: 'Sohbetteki dosya bağlantılarını (araç satırları, üretilen dosyalar, anılmalar) sistem varsayılan uygulaması yerine kenar çubuğu düzenleyicisinde aç', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Model için kenar çubuğu açma aracı enjekte et', settingsOpenToolsDesc: 'Etkinleştirildiğinde model, sidebar_open aracıyla kenar çubuğunda dosyaları, klasörleri ve HTTP(S) sayfalarını açabilir (varsayılan kapalı)', settingsTitleBarTitle: 'Konum uyumluluk modu', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 94c8e6ab9..b7d4a41f0 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -189,6 +189,8 @@ export const vi: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Mở tệp chat trong thanh bên', settingsOpenPathDesc: 'Khi nhấp link tệp trong chat (dòng công cụ, danh sách sản phẩm, nhắc tệp), mở trong trình soạn thảo thanh bên thay vì ứng dụng mặc định hệ thống', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Tiêm công cụ mở thanh bên cho mô hình', settingsOpenToolsDesc: 'Khi bật, mô hình có thể mở tệp, thư mục và trang HTTP(S) trong thanh bên qua công cụ sidebar_open (mặc định tắt)', settingsTitleBarTitle: 'Chế độ tương thích thanh tiêu đề', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index 7e8cb9413..ea339b3e9 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -204,6 +204,8 @@ export const zhHK: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: '聊天區檔案在側邊欄開啟', settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: '為模型注入側邊欄開啟工具', settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)', settingsTitleBarTitle: '位置相容模式', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index e21f71b61..19b81f995 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -204,6 +204,8 @@ export const zhMO: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: '聊天區檔案在側邊欄開啟', settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: '為模型注入側邊欄開啟工具', settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)', settingsTitleBarTitle: '位置相容模式', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 2d81e3ffe..d4d4eb1d3 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -204,6 +204,8 @@ export const zhTW: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: '聊天區檔案在側邊欄開啟', settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: '為模型注入側邊欄開啟工具', settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)', settingsTitleBarTitle: '位置相容模式', diff --git a/src/client/locales.ts b/src/client/locales.ts index 23ee8f778..032f2ff33 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -196,6 +196,8 @@ export const zh = { settingsWidthSuffix: '%', settingsOpenPathTitle: '聊天区文件在侧边栏打开', settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用', + settingsEditDiffTitle: '编辑类文件以 diff 视图打开', + settingsEditDiffDesc: '开启后,点击编辑工具的文件链接时在侧边栏打开 git 工作区 diff 视图,而不是直接打开文件;非 git 仓库自动回退为文件打开', settingsOpenToolsTitle: '为模型注入侧边栏打开工具', settingsOpenToolsDesc: '开启后,模型可通过 sidebar_open 工具在侧边栏主动打开文件、文件夹和 HTTP(S) 网页(默认关闭)', settingsTitleBarTitle: '位置兼容模式', @@ -540,6 +542,8 @@ export const en: Record = { settingsWidthSuffix: '%', settingsOpenPathTitle: 'Open chat files in the sidebar', settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app', + settingsEditDiffTitle: 'Open edited files as diff', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Inject the sidebar-open tool for the model', settingsOpenToolsDesc: 'When enabled, the model can actively open files, folders, and HTTP(S) pages in the sidebar through the sidebar_open tool (off by default)', settingsTitleBarTitle: 'Position compatibility mode', diff --git a/src/client/openpath-intercept.ts b/src/client/openpath-intercept.ts index 4bac26512..4924f59d5 100644 --- a/src/client/openpath-intercept.ts +++ b/src/client/openpath-intercept.ts @@ -29,9 +29,9 @@ export interface OpenPathInterceptDeps { /** The session whose scope the sidebar editor loads the file in (current session). */ currentSessionId(): string | undefined /** Route the open into the sidebar editor (the established openSidebarFile). */ - openInSidebar(path: string, sessionId: string): void + openInSidebar(path: string, sessionId: string): void | Promise /** Route a folder-reveal gesture ("Show in folder" passes '.') into the sidebar explorer. */ - revealInExplorer(path: string, sessionId: string): void + revealInExplorer(path: string, sessionId: string): void | Promise } /** diff --git a/src/client/prefs.ts b/src/client/prefs.ts index a855109d9..925359a7d 100644 --- a/src/client/prefs.ts +++ b/src/client/prefs.ts @@ -79,6 +79,9 @@ export function parsePrefs(value: unknown): SidebarPrefs { interceptOpenPath: typeof record.interceptOpenPath === 'boolean' ? record.interceptOpenPath : SIDEBAR_PREFS_DEFAULTS.interceptOpenPath, + editOpensDiff: typeof record.editOpensDiff === 'boolean' + ? record.editOpensDiff + : SIDEBAR_PREFS_DEFAULTS.editOpensDiff, editorExplorer: typeof record.editorExplorer === 'boolean' ? record.editorExplorer : SIDEBAR_PREFS_DEFAULTS.editorExplorer, diff --git a/src/client/service.ts b/src/client/service.ts index 1c315991e..14224973e 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -707,14 +707,17 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe // own panel opens — the bottom panel when the active pane lives in the // bottom tree, else the right panel. Type-only opens (+ menu, // agent-terminal auto-tabs) never expand (the panel behavior is their - // caller's business). The check runs on the post-dedupe state, so a - // content open that merely FOCUSES an existing tab expands the panel - // too — the open must never land out of sight. Opens targeted at an - // INACTIVE session never expand (nothing is in sight for the user). + // caller's business). A diff seed is content too (it carries `diff`, + // not `path`/`url`) — an edit-tool open that lands the diff tab in a + // collapsed panel would otherwise stay out of sight. The check runs + // on the post-dedupe state, so a content open that merely FOCUSES an + // existing tab expands the panel too — the open must never land out + // of sight. Opens targeted at an INACTIVE session never expand + // (nothing is in sight for the user). if ( !targetsInactiveSession && typeof window !== 'undefined' - && (seed.path !== undefined || seed.url !== undefined) + && (seed.path !== undefined || seed.url !== undefined || seed.diff !== undefined) ) { if (isNarrowWidth(window.innerWidth)) { if (!landed.panelOpen) return togglePanel(landed) diff --git a/src/config.ts b/src/config.ts index 6164b6582..600d2b153 100644 --- a/src/config.ts +++ b/src/config.ts @@ -195,6 +195,7 @@ export const PrefsSchema: z = z.object({ terminalFontFamily: z.string().default(''), terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT), interceptOpenPath: z.boolean().default(true), + editOpensDiff: z.boolean().default(true), editorExplorer: z.boolean().default(false), terminalShell: z.string().default(''), terminalShellArgs: z.string().default(''), diff --git a/src/git.ts b/src/git.ts index 9b9f151cf..6cabcc6dc 100644 --- a/src/git.ts +++ b/src/git.ts @@ -10,7 +10,7 @@ * user.name/user.email). */ import { readdir } from 'node:fs/promises' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { spawn } from 'node:child_process' import { resolve } from 'node:path' @@ -222,6 +222,34 @@ async function directRepoRoot(cwd: string): Promise { return out.trim() } +/** + * Resolve the repository root that contains `path`. The lookup runs + * `git rev-parse --show-toplevel` against the directory that holds `path` + * (and, when `path` itself is a repository root, against `path` itself), so + * callers can map an arbitrary absolute file path to its owning checkout + * without first guessing the session's discovered roots. + * + * @param path - Absolute file or directory path whose repository is desired. + * @returns The canonical repository root, or undefined when `path` is not + * inside a git work tree or the git probe fails. Never throws. + */ +export async function repoRootOf(path: string): Promise { + const candidates: string[] = [] + const dir = dirname(path) + candidates.push(dir) + if (dir !== path) candidates.push(path) + for (const cwd of candidates) { + try { + const out = await runGit(cwd, ['rev-parse', '--show-toplevel'], DISCOVERY_TIMEOUT_MS) + const trimmed = out.trim() + if (trimmed !== '') return trimmed + } catch { + // Not a repository at this candidate — try the next one. + } + } + return undefined +} + /** Discover the current repository or direct child repositories. Results are * cached per cwd and concurrent callers share one in-flight scan, so opening * the panel (three parallel git.* requests) costs a single discovery pass. */ @@ -318,7 +346,7 @@ export async function status(cwd: string, selected?: string): Promise { + const { cwd } = cwdOf(payload) + const raw = requireString(payload, 'path') + const absolute = await ensureWorkspacePath(cwd, raw, resolved.extraRoots) + const root = await git.repoRootOf(absolute) + if (root === undefined) return { isRepo: false, entries: [] } + return git.status(root) + }, 'git.diff': async (payload) => { const { cwd } = await gitCwdOf(payload) const record = payload as { path?: unknown; staged?: unknown } const repoRoot = selectedRepoOf(payload) - const path = record.path === undefined ? undefined : await resolveGitPath(cwd, requireString(payload, 'path'), repoRoot) + const hasPath = record.path !== undefined + const rawPath = hasPath ? requireString(payload, 'path') : undefined + if (repoRoot !== undefined) { + const roots = await git.repoRoots(cwd) + const isKnown = roots.some(root => git.pathIdentity(root) === git.pathIdentity(repoRoot)) + if (!isKnown) { + await ensureWorkspacePath(cwd, repoRoot, resolved.extraRoots) + const actual = await git.repoRootOf(repoRoot) + if (actual === undefined) { + throw new git.GitCommandError(`not a git repository: ${repoRoot}`, 'not-repo', 'rev-parse') + } + let path: string | undefined + if (rawPath !== undefined) { + if (isAbsolute(rawPath)) { + path = requireAbsolute(rawPath) + } else { + // Attempts to resolve relative paths against the external repo root + // directly, instead of the session cwd, so a file like + // "src/a.ts" inside the external checkout does not silently fall + // back to the session's first discovered root. + path = requireAbsolute(join(actual, rawPath)) + } + } + return { diff: await git.diff(actual, path, record.staged === true) } + } + } + const path = rawPath === undefined ? undefined : await resolveGitPath(cwd, rawPath, repoRoot) return { diff: await git.diff(cwd, path, record.staged === true, repoRoot) } }, 'git.stage': async (payload) => { diff --git a/src/prefs-shared.ts b/src/prefs-shared.ts index 0b4408417..479f569b1 100644 --- a/src/prefs-shared.ts +++ b/src/prefs-shared.ts @@ -68,6 +68,12 @@ export interface SidebarPrefs { * own enable switch gates it too (both must be on for the takeover). */ interceptOpenPath: boolean + /** + * Whether edit-tool path clicks (tool-row `openPath` interception) open the + * git worktree diff view instead of the file editor. On by default; when off + * the click opens the file in the editor exactly as before. + */ + editOpensDiff: boolean /** * Whether the editor tab runs in merged mode: a path input replaces the * plain header and a toggleable file-tree panel (with a global name @@ -252,6 +258,7 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = { terminalFontFamily: '', terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT, interceptOpenPath: true, + editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', diff --git a/tests/bundle-route.spec.ts b/tests/bundle-route.spec.ts index faf44003a..de440f35c 100644 --- a/tests/bundle-route.spec.ts +++ b/tests/bundle-route.spec.ts @@ -84,7 +84,10 @@ describe('/sidebar/bundle route', () => { try { const first = fakeRes() await handler(req('GET', '/sidebar/bundle/editor.js'), first as unknown as ServerResponse) - writeFileSync(join(dir, 'client-editor.js'), 'window.__ModuleLoader__ && 1;') + // Use a different size so the mtime+size memo invalidates even when the + // two writes land in the same mtimeMs tick (same-second write would + // otherwise return the memoized ETag and incorrectly 304). + writeFileSync(join(dir, 'client-editor.js'), 'window.__ModuleLoader__ && 1; // changed') const second = fakeRes() await handler(req('GET', '/sidebar/bundle/editor.js', { 'if-none-match': first.headers.etag! }), second as unknown as ServerResponse) expect(second.status).toBe(200) diff --git a/tests/edit-diff.spec.ts b/tests/edit-diff.spec.ts new file mode 100644 index 000000000..3df3dd422 --- /dev/null +++ b/tests/edit-diff.spec.ts @@ -0,0 +1,434 @@ +import { describe, expect, it } from 'vitest' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve as resolvePath } from 'node:path' +import { deriveEditDiffTarget, buildEditDiffTab } from '../src/client/edit-diff.ts' +import type { GitStatusResult } from '../src/client/api.ts' +import * as git from '../src/git.ts' +import { apply } from '../src/index.ts' +import type { SidebarWebRoute, SidebarWebUpgradeRoute } from '../src/context-types.ts' + +const normalizePath = (path: string): string => path.replaceAll('\\', '/') +const canonical = (path: string): string => normalizePath(realpathSync(path)) + +// Helpers for scratch repos (same identity as smoke) +const FIXTURE_IDENTITY = { + GIT_AUTHOR_NAME: 'dsh-better-sidebar-test', + GIT_AUTHOR_EMAIL: 'test@dsh.invalid', + GIT_COMMITTER_NAME: 'dsh-better-sidebar-test', + GIT_COMMITTER_EMAIL: 'test@dsh.invalid', +} + +function gitRun(cwd: string, args: string[]): string { + const result = spawnSync('git', ['-C', cwd, '--no-pager', '-c', 'color.ui=false', ...args], { + encoding: 'utf8', + env: { ...process.env, ...FIXTURE_IDENTITY }, + }) + if (result.status !== 0) { + throw new Error(result.stderr || `git ${args[0] ?? ''} exited with ${String(result.status)}`) + } + return result.stdout +} + +function makeScratchRepo(): string { + const dir = mkdtempSync(join(tmpdir(), 'dsh-editdiff-')) + gitRun(dir, ['init', '-q']) + gitRun(dir, ['config', 'user.email', 'test@test']) + gitRun(dir, ['config', 'user.name', 'test']) + // ensure branch stable + gitRun(dir, ['checkout', '-q', '-b', 'main']) + writeFileSync(join(dir, 'a.txt'), 'one\ntwo\nthree\n') + gitRun(dir, ['add', '-A']) + gitRun(dir, ['commit', '-q', '-m', 'base']) + return dir +} + +// ── Client derive/build ───────────────────────────────────────────── + +describe('deriveEditDiffTarget', () => { + const root = '/repo' + const statusWith = (entries: GitStatusResult['entries'], isRepo = true, rootParam: string | undefined = root): GitStatusResult => ({ + isRepo, + root: rootParam, + entries, + }) + const statusWithUndefinedRoot = (entries: GitStatusResult['entries']): GitStatusResult => ({ + isRepo: true, + root: undefined, + entries, + }) + + it('returns target when file is inside repo and has a status entry', () => { + const absolute = join(root, 'src/a.ts') + const status = statusWith([{ path: 'src/a.ts', xy: ' M' }]) + const target = deriveEditDiffTarget(absolute, status) + expect(target).toEqual({ relative: 'src/a.ts', repoRoot: root, untracked: false }) + }) + + it('returns null when file has no status entry', () => { + const absolute = join(root, 'src/a.ts') + const status = statusWith([{ path: 'src/b.ts', xy: ' M' }]) + expect(deriveEditDiffTarget(absolute, status)).toBeNull() + }) + + it('returns null when isRepo false', () => { + const absolute = join(root, 'src/a.ts') + const status: GitStatusResult = { isRepo: false, entries: [] } + expect(deriveEditDiffTarget(absolute, status)).toBeNull() + }) + + it('returns null when root is undefined', () => { + const absolute = join(root, 'src/a.ts') + const status = statusWithUndefinedRoot([{ path: 'src/a.ts', xy: ' M' }]) + expect(deriveEditDiffTarget(absolute, status)).toBeNull() + }) + + it('marks untracked (??) as untracked:true', () => { + const absolute = join(root, 'new.txt') + const status = statusWith([{ path: 'new.txt', xy: '??' }]) + const target = deriveEditDiffTarget(absolute, status)! + expect(target.untracked).toBe(true) + expect(target.relative).toBe('new.txt') + }) + + it('returns null when file is outside repo root', () => { + const absolute = '/other/src/a.ts' + const status = statusWith([{ path: 'src/a.ts', xy: ' M' }]) + expect(deriveEditDiffTarget(absolute, status)).toBeNull() + }) + + it('returns null when absolute equals repo root', () => { + const absolute = root + const status = statusWith([{ path: 'src/a.ts', xy: ' M' }]) + expect(deriveEditDiffTarget(absolute, status)).toBeNull() + }) + + it('handles staged and modified xy codes', () => { + const absolute = join(root, 'src/mod.ts') + for (const xy of ['M ', 'AM', 'MM', 'R ', 'C ']) { + const status = statusWith([{ path: 'src/mod.ts', xy }]) + const target = deriveEditDiffTarget(absolute, status) + expect(target, xy).not.toBeNull() + expect(target!.untracked).toBe(false) + } + }) +}) + +describe('buildEditDiffTab', () => { + it('builds id/title/diff ref with repoRoot transparent', () => { + const tab = buildEditDiffTab('src/a.ts', '/repo', false) + expect(tab.id).toBe('diff:w::u:src/a.ts') + expect(tab.type).toBe('diff') + expect(tab.title).toBe('a.ts') + expect(tab.diff).toEqual({ kind: 'worktree', path: 'src/a.ts', staged: false, untracked: false, repoRoot: '/repo' }) + }) + + it('preserves untracked flag', () => { + const tab = buildEditDiffTab('new.txt', '/repo', true) + expect((tab.diff as { untracked: boolean; staged: boolean }).untracked).toBe(true) + expect((tab.diff as { untracked: boolean; staged: boolean }).staged).toBe(false) + }) + + it('handles nested paths for title', () => { + const tab = buildEditDiffTab('a/b/c/deep.ts', '/repo', false) + expect(tab.title).toBe('deep.ts') + expect(tab.id).toBe('diff:w::u:a/b/c/deep.ts') + }) +}) + +// ── git.repoRootOf ───────────────────────────────────────────────── + +describe('git.repoRootOf', () => { + it('resolves repo root for a file inside a repo', async () => { + const repo = makeScratchRepo() + try { + const file = join(repo, 'a.txt') + const root = await git.repoRootOf(file) + expect(root).toBeDefined() + expect(canonical(root!)).toBe(canonical(repo)) + } finally { + rmSync(repo, { recursive: true, force: true }) + } + }) + + it('resolves repo root for a nested file', async () => { + const repo = makeScratchRepo() + try { + mkdirSync(join(repo, 'src', 'sub'), { recursive: true }) + const file = join(repo, 'src', 'sub', 'nested.ts') + writeFileSync(file, 'x') + const root = await git.repoRootOf(file) + expect(canonical(root!)).toBe(canonical(repo)) + } finally { + rmSync(repo, { recursive: true, force: true }) + } + }) + + it('resolves repo root when given the repo root directory itself', async () => { + const repo = makeScratchRepo() + try { + const root = await git.repoRootOf(repo) + expect(root).toBeDefined() + expect(canonical(root!)).toBe(canonical(repo)) + } finally { + rmSync(repo, { recursive: true, force: true }) + } + }) + + it('returns undefined for a path outside any repo', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-editdiff-norepo-')) + try { + const file = join(dir, 'lonely.txt') + writeFileSync(file, 'hello') + const root = await git.repoRootOf(file) + expect(root).toBeUndefined() + } finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('returns undefined for non-existent path outside repo', async () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-editdiff-norepo2-')) + try { + const file = join(dir, 'missing.txt') + const root = await git.repoRootOf(file) + expect(root).toBeUndefined() + } finally { + rmSync(dir, { recursive: true, force: true }) + } + }) +}) + +// ── Server API helpers ────────────────────────────────────────────── + +function mountApi(cwd: string, extraRoots: string[] = []): SidebarWebRoute { + const routes: SidebarWebRoute[] = [] + const ctx = { + webRuntime: { trustedHosts: [] as readonly string[] }, + webServer: { + register: (route: SidebarWebRoute) => { routes.push(route); return () => {} }, + registerUpgrade: (route: SidebarWebUpgradeRoute) => { void route; return () => {} }, + }, + sessions: { get: (id: string) => id === 's' ? { header: { cwd } } : undefined }, + tools: { register: () => () => {} }, + effect: (fn: () => void | (() => void)) => { fn() }, + inject: () => () => {}, + get: () => undefined, + } + apply(ctx as never, { extraRoots }) + const api = routes.find(r => r.path === '/sidebar/api') + if (!api) throw new Error('api route not mounted') + return api +} + +async function invoke( + route: SidebarWebRoute, + method: string, + payload: unknown, +): Promise<{ ok: boolean; status: number; value?: unknown; error?: { code?: string; message: string } }> { + const body = Buffer.from(JSON.stringify(payload)) + const req = { + method: 'POST', + url: `/sidebar/api/${method}`, + headers: { host: '127.0.0.1:3080' }, + [Symbol.asyncIterator]: async function* () { yield body }, + } as never + const out: { status: number; body: string } = { status: 200, body: '' } + const res = { + writeHead: (status: number) => { out.status = status }, + end: (chunk: unknown) => { out.body += String(chunk ?? '') }, + } as never + await route.handler(req, res) + const parsed = JSON.parse(out.body) as { ok: boolean; value?: unknown; error?: { code?: string; message: string } } + return { ...parsed, status: out.status } +} + +// ── git.status-at route ──────────────────────────────────────────── + +describe('git.status-at route', () => { + it('returns isRepo true for a file inside a repo', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-statusat-session-')) + const repo = makeScratchRepo() + try { + // modify repo so status has entries + writeFileSync(join(repo, 'a.txt'), 'modified\n') + // also an untracked file + writeFileSync(join(repo, 'untracked.txt'), 'new') + const extraRoots = [repo] + const api = mountApi(sessionRoot, extraRoots) + const file = join(repo, 'a.txt') + const result = await invoke(api, 'git.status-at', { sessionId: 's', cwd: sessionRoot, path: file }) + expect(result.ok).toBe(true) + const value = result.value as GitStatusResult + expect(value.isRepo).toBe(true) + expect(value.root).toBeDefined() + expect(canonical(value.root!)).toBe(canonical(repo)) + expect(value.entries.some(e => e.path === 'a.txt')).toBe(true) + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(repo, { recursive: true, force: true }) + } + }) + + it('returns isRepo false for a path not in a repo but inside allowed root', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-statusat-norepo-session-')) + const allowed = mkdtempSync(join(tmpdir(), 'dsh-statusat-allowed-')) + try { + const file = join(allowed, 'plain.txt') + writeFileSync(file, 'hello') + const api = mountApi(sessionRoot, [allowed]) + const result = await invoke(api, 'git.status-at', { sessionId: 's', cwd: sessionRoot, path: file }) + expect(result.ok).toBe(true) + const value = result.value as GitStatusResult + expect(value.isRepo).toBe(false) + expect(value.entries).toEqual([]) + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(allowed, { recursive: true, force: true }) + } + }) + + it('rejects with 403 when path is outside workspace and extraRoots', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-statusat-forbidden-session-')) + const allowed = mkdtempSync(join(tmpdir(), 'dsh-statusat-allowed2-')) + const outside = mkdtempSync(join(tmpdir(), 'dsh-statusat-outside-')) + try { + const file = join(outside, 'secret.txt') + writeFileSync(file, 'secret') + const api = mountApi(sessionRoot, [allowed]) + const result = await invoke(api, 'git.status-at', { sessionId: 's', cwd: sessionRoot, path: file }) + expect(result.ok).toBe(false) + expect(result.status).toBe(403) + expect(result.error?.code).toBe('forbidden') + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(allowed, { recursive: true, force: true }) + rmSync(outside, { recursive: true, force: true }) + } + }) + + it('returns isRepo false for a workspace file not in a repo (no extraRoots needed)', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-statusat-workspace-plain-')) + try { + const file = join(sessionRoot, 'plain.txt') + writeFileSync(file, 'x') + const api = mountApi(sessionRoot, []) + const result = await invoke(api, 'git.status-at', { sessionId: 's', cwd: sessionRoot, path: file }) + expect(result.ok).toBe(true) + expect((result.value as GitStatusResult).isRepo).toBe(false) + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + } + }) +}) + +// ── git.diff external repoRoot ───────────────────────────────────── + +describe('git.diff external repoRoot', () => { + it('returns diff for external repo when extraRoots allows it', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-diff-session-')) + const external = makeScratchRepo() + try { + const rel = 'a.txt' + // create an unstaged change + writeFileSync(join(external, rel), 'one\nCHANGED\nthree\n') + const api = mountApi(sessionRoot, [external]) + const result = await invoke(api, 'git.diff', { sessionId: 's', cwd: sessionRoot, path: rel, staged: false, repoRoot: external }) + expect(result.ok).toBe(true) + const value = result.value as { diff: string } + expect(value.diff).toContain('CHANGED') + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(external, { recursive: true, force: true }) + } + }) + + it('rejects with 403 when external repoRoot is outside extraRoots', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-diff-session2-')) + const external = makeScratchRepo() + const otherExtra = mkdtempSync(join(tmpdir(), 'dsh-diff-extra-other-')) + try { + writeFileSync(join(external, 'a.txt'), 'one\nCHANGED\nthree\n') + const api = mountApi(sessionRoot, [otherExtra]) + const result = await invoke(api, 'git.diff', { sessionId: 's', cwd: sessionRoot, path: 'a.txt', staged: false, repoRoot: external }) + expect(result.ok).toBe(false) + expect(result.status).toBe(403) + expect(result.error?.code).toBe('forbidden') + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(external, { recursive: true, force: true }) + rmSync(otherExtra, { recursive: true, force: true }) + } + }) + + it('rejects when external repoRoot is not a git repo even if fenced', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-diff-session3-')) + const notRepo = mkdtempSync(join(tmpdir(), 'dsh-diff-notrepo-')) + try { + writeFileSync(join(notRepo, 'file.txt'), 'x') + const api = mountApi(sessionRoot, [notRepo]) + const result = await invoke(api, 'git.diff', { sessionId: 's', cwd: sessionRoot, path: 'file.txt', staged: false, repoRoot: notRepo }) + expect(result.ok).toBe(false) + // GitCommandError surfaces as internal (writeError) or git-error depending on wiring + expect(result.error).toBeDefined() + expect(result.status).toBeGreaterThanOrEqual(400) + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(notRepo, { recursive: true, force: true }) + } + }) + + it('keeps old behavior for repoRoot inside discovered list', async () => { + // workspace container with two child repos; session cwd is the container + const workspace = mkdtempSync(join(tmpdir(), 'dsh-diff-container-')) + const first = join(workspace, 'first-repo') + const second = join(workspace, 'second-repo') + mkdirSync(first) + mkdirSync(second) + gitRun(first, ['init', '-q']) + gitRun(first, ['config', 'user.email', 't@t']) + gitRun(first, ['config', 'user.name', 't']) + gitRun(first, ['commit', '-q', '--allow-empty', '-m', 'init']) + + gitRun(second, ['init', '-q']) + gitRun(second, ['config', 'user.email', 't@t']) + gitRun(second, ['config', 'user.name', 't']) + gitRun(second, ['checkout', '-q', '-b', 'main']) + writeFileSync(join(second, 'b.txt'), 'base\n') + gitRun(second, ['add', '-A']) + gitRun(second, ['commit', '-q', '-m', 'base']) + writeFileSync(join(second, 'b.txt'), 'modified\n') + + try { + // No extraRoots needed; second is discovered as child of workspace + const api = mountApi(workspace, []) + const result = await invoke(api, 'git.diff', { sessionId: 's', cwd: workspace, path: 'b.txt', staged: false, repoRoot: canonical(second) }) + expect(result.ok).toBe(true) + const value = result.value as { diff: string } + expect(value.diff).toContain('modified') + } finally { + rmSync(workspace, { recursive: true, force: true }) + } + }) + + it('resolves relative path against external repo root (not session cwd)', async () => { + const sessionRoot = mkdtempSync(join(tmpdir(), 'dsh-diff-session-rel-')) + const external = makeScratchRepo() + // add nested file + mkdirSync(join(external, 'src'), { recursive: true }) + writeFileSync(join(external, 'src', 'nested.ts'), 'orig\n') + gitRun(external, ['add', '-A']) + gitRun(external, ['commit', '-q', '-m', 'add nested']) + writeFileSync(join(external, 'src', 'nested.ts'), 'changed\n') + try { + const api = mountApi(sessionRoot, [external]) + const result = await invoke(api, 'git.diff', { sessionId: 's', cwd: sessionRoot, path: 'src/nested.ts', staged: false, repoRoot: external }) + expect(result.ok).toBe(true) + expect((result.value as { diff: string }).diff).toContain('changed') + } finally { + rmSync(sessionRoot, { recursive: true, force: true }) + rmSync(external, { recursive: true, force: true }) + } + }) +}) diff --git a/tests/openpath-intercept.spec.ts b/tests/openpath-intercept.spec.ts index 619928b57..cc6fec6f1 100644 --- a/tests/openpath-intercept.spec.ts +++ b/tests/openpath-intercept.spec.ts @@ -114,10 +114,14 @@ describe('open-path interception wiring', () => { : undefined, } as unknown as Context const store = createSidebarStore() + // The edit→diff pref defaults on, but this wiring spec pins the legacy + // editor routing (the diff path is async and needs a git probe). Disable + // the diff pref so the open lands synchronously in the editor as before. + store.setPrefs({ ...store.getPrefs(), editOpensDiff: false }) const original = ctx.workspaces.openPath const restore = registerOpenPathInterception(ctx, store) - // Default prefs: the takeover routes the open into the sidebar editor + // Default prefs (with edit diff off): the takeover routes the open into the sidebar editor // with the session-scoped absolute path (chat already resolved it). await ctx.workspaces.openPath('/w/src/a.ts') expect(opened).toEqual([{ diff --git a/tests/plugin-shape.spec.ts b/tests/plugin-shape.spec.ts index 81c1fb034..cf58bb370 100644 --- a/tests/plugin-shape.spec.ts +++ b/tests/plugin-shape.spec.ts @@ -94,6 +94,6 @@ describe('dsh-better-sidebar plugin export shape', () => { const overridden = (PrefsSchema as unknown as { (input: Record | undefined): Record })({ openByDefault: false, defaultWidthPercent: 45 }) - expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: '', tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) }) }) diff --git a/tests/prefs.spec.ts b/tests/prefs.spec.ts index 4226128a0..34d457c3b 100644 --- a/tests/prefs.spec.ts +++ b/tests/prefs.spec.ts @@ -42,6 +42,7 @@ describe('side card preferences', () => { terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, + editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', @@ -75,6 +76,7 @@ describe('side card preferences', () => { terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, + editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', @@ -108,6 +110,7 @@ describe('side card preferences', () => { terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, + editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', @@ -270,9 +273,9 @@ describe('side card preferences', () => { const store = createSidebarStore() // Node environment: no window → the width falls back to PANEL_DEFAULT, // while the open flag still follows the preference. - store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) store.setSession('fresh-session') - expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) const snapshot = store.getSnapshot() expect(snapshot.sessionId).toBe('fresh-session') expect(snapshot.state?.panelOpen).toBe(false) @@ -308,7 +311,7 @@ describe('side card preferences', () => { it('skips the default seed tab when the editor (files window) type is disabled', () => { const store = createSidebarStore() - store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} }) + store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer: true, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: { editor: false }, viewersEnabled: {}, pluginSettings: {} }) store.setSession('no-editor') const state = store.getSnapshot().state! const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs) @@ -318,7 +321,7 @@ describe('side card preferences', () => { // editorExplorer modes. for (const editorExplorer of [true, false]) { const openStore = createSidebarStore() - openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) openStore.setSession(`with-editor-${editorExplorer}`) const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs) expect(openTabs.map(tab => tab.type)).toEqual(['editor']) @@ -328,7 +331,7 @@ describe('side card preferences', () => { it('seeds the empty editor home tab (files window) in both editorExplorer modes', () => { for (const editorExplorer of [true, false]) { const store = createSidebarStore() - store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) + store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, agentOpenTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, editOpensDiff: true, editorExplorer, terminalShell: '', terminalShellArgs: '', titleBarScheme: 'auto', titleBarPresetId: '', customCss: '', titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, browserAllowedLoopback: "", tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} }) store.setSession(`fresh-${editorExplorer}`) const tabs = allLeaves(store.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs) expect(tabs).toHaveLength(1) diff --git a/tests/service.spec.ts b/tests/service.spec.ts index 728280322..4ff8eb3a7 100644 --- a/tests/service.spec.ts +++ b/tests/service.spec.ts @@ -557,6 +557,24 @@ describe('service.openTab auto-expand for content opens', () => { } }) + it('expands the collapsed drawer for a diff open on a narrow viewport', () => { + setWidth(390) + try { + const store = createSidebarStore() + const service = createBetterSidebarService(store) + service.registerTab({ id: 'diff', title: 'Git', component: () => null }) + store.setSession('s1') + store.reduce(s => ({ ...s, panelOpen: false })) + // A diff seed carries `diff`, not `path`/`url` — it is still a content + // open (an edit-tool click landing the diff in a collapsed panel must + // surface, not stay out of sight). + service.openTab({ type: 'diff', title: 'a.ts', diff: { kind: 'worktree', path: 'a.ts', staged: false } }) + expect(store.getSnapshot().state?.panelOpen).toBe(true) + } finally { + setWidth(1024) + } + }) + it('keeps a collapsed drawer for a type-only open on a narrow viewport', () => { setWidth(390) try { diff --git a/tests/side-card-section.spec.tsx b/tests/side-card-section.spec.tsx index 3e55bc3d3..9b5566e51 100644 --- a/tests/side-card-section.spec.tsx +++ b/tests/side-card-section.spec.tsx @@ -96,8 +96,8 @@ describe('SideCardSection declarative inventory', () => { // The nested auto-open toggle is NOT an inline card (it lives in the popup). expect(pressedCount(html, 'true')).toBe(3) expect(pressedCount(html, 'false')).toBe(0) - // The general toggles are custom switches (real checkboxes, one checked). - expect(html.match(/checked=""/g)?.length).toBe(1) + // The general toggles are custom switches (real checkboxes, two checked). + expect(html.match(/checked=""/g)?.length).toBe(2) expect(html).not.toContain('Auto-open Subagents') }) @@ -153,9 +153,9 @@ describe('SideCardSection declarative inventory', () => { expect(html).toContain('>Subagents<') expect(html).toContain('>Image<') expect(pressedCount(html, 'false')).toBe(2) - // The explorer card stays pressed; the one default-on general switch stays checked. + // The explorer card stays pressed; the two default-on general switches stay checked. expect(pressedCount(html, 'true')).toBe(1) - expect(html.match(/checked=""/g)?.length).toBe(1) + expect(html.match(/checked=""/g)?.length).toBe(2) }) it('hides the gear of a disabled feature (its related settings are dormant)', () => { @@ -178,11 +178,11 @@ describe('SideCardSection declarative inventory', () => { expect(html).toContain('Pick the title-bar compatibility scheme: auto-detect (default, conservative) / DSH official web / known desktop shells / custom (shift distance + custom CSS)') expect(html).not.toContain('Auto-detect<') - // Three general-row switches remain (openByDefault + interceptOpenPath - // + agentOpenTools), only interceptOpenPath checked by default — the + // Four general-row switches remain (openByDefault + interceptOpenPath + // + editOpensDiff + agentOpenTools), interceptOpenPath + editOpensDiff checked by default — the // scheme row is a dropdown, not a switch. - expect(html.match(/type="checkbox"/g)?.length).toBe(3) - expect(html.match(/checked=""/g)?.length).toBe(1) + expect(html.match(/type="checkbox"/g)?.length).toBe(4) + expect(html.match(/checked=""/g)?.length).toBe(2) // Auto (default) needs no further settings → no gear. expect(html).not.toContain('Position compatibility mode Feature settings') diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index 2f986c423..fce9728e3 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -899,6 +899,7 @@ describe('side card settings routes', () => { terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, + editOpensDiff: true, editorExplorer: false, terminalShell: '', terminalShellArgs: '', diff --git a/tests/turn-tail-intercept.spec.ts b/tests/turn-tail-intercept.spec.ts index 352100c44..5bbf3bdd2 100644 --- a/tests/turn-tail-intercept.spec.ts +++ b/tests/turn-tail-intercept.spec.ts @@ -170,6 +170,7 @@ describe('turn-tail interception registration (issue #15)', () => { const fake = fakeSlots(true) const ctx = clientCtx(fake.slots) const store = createSidebarStore() + store.setPrefs({ ...store.getPrefs(), editOpensDiff: false }) const restore = registerTurnTailInterception(ctx, store) const inject = fake.registered[0]!.options.inject as (sessionId: string) => { openInSidebar: (path: string) => void From a77c91bd0866192174fc9611381f9ac37ab9c4a6 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Thu, 27 Aug 2026 10:55:50 +0800 Subject: [PATCH 03/11] fix(terminal): focus on host click and let paste gestures reach xterm --- src/client/TerminalView.tsx | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/client/TerminalView.tsx b/src/client/TerminalView.tsx index a226fc92e..12c4ba0e9 100644 --- a/src/client/TerminalView.tsx +++ b/src/client/TerminalView.tsx @@ -228,6 +228,52 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store: const inputSub = term.onData((data) => { if (socket !== null && socket.readyState === WebSocket.OPEN) socket.send(data) }) + + // 聚焦保障:点击终端壳即 focus 到 xterm 的 textarea + const onHostClick = (): void => term.focus() + host.addEventListener('click', onHostClick) + + // 让 Ctrl+V / Ctrl+Shift+V / Shift+Insert 穿透给浏览器而不是被 xterm 吞掉 + term.attachCustomKeyEventHandler((e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') return false + if (e.shiftKey && e.key === 'Insert') return false + // Ctrl+Shift+V 在某些 Linux 终端也是粘贴,同样放行 + if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'v') return false + return true + }) + + // 粘贴:同步 clipboardData 优先,空时用 Edge 异步兜底,经 term.paste() 走 onData->ws + const onPaste = (e: ClipboardEvent): void => { + const text = e.clipboardData?.getData('text/plain') + if (text) { + e.preventDefault() + term.paste(text) + return + } + // Edge 增强粘贴:clipboardData 为空(如跨应用、云剪贴板),用 async API + if (navigator.clipboard?.readText) { + e.preventDefault() + void navigator.clipboard.readText().then((t) => { if (t) term.paste(t) }).catch(() => {}) + } + } + host.addEventListener('paste', onPaste) + + // 额外兜底:焦点不在 textarea 时,Ctrl+V 的 keydown 仍可触发 async 读(Edge 失焦场景) + const onKeyDownPasteFallback = (e: KeyboardEvent): void => { + const isPasteGesture = ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 'v') + || (e.shiftKey && e.key === 'Insert') + if (!isPasteGesture) return + const ae = document.activeElement as HTMLElement | null + const isTextarea = ae?.classList.contains('xterm-helper-textarea') ?? false + if (isTextarea) return // 已有 paste 事件,无需重复 + if (!navigator.clipboard?.readText) return + // 只在 host 包含焦点或 host 自身被点击过的场景下兜底,避免全局劫持其他输入框的粘贴 + if (!host.contains(ae) && ae !== document.body) return + e.preventDefault() + void navigator.clipboard.readText().then((t) => { if (t) { term.focus(); term.paste(t) } }).catch(() => {}) + } + host.addEventListener('keydown', onKeyDownPasteFallback as unknown as EventListener) + const observer = new ResizeObserver(() => { try { fit.fit() @@ -283,6 +329,9 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store: cancelOpen() window.clearTimeout(retry) observer.disconnect() + host.removeEventListener('click', onHostClick) + host.removeEventListener('paste', onPaste) + host.removeEventListener('keydown', onKeyDownPasteFallback as unknown as EventListener) fontSub() schemeSub() inputSub.dispose() From da16a44c16b8f8c1bb634996e5b3f164bf442132 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Thu, 27 Aug 2026 11:32:29 +0800 Subject: [PATCH 04/11] feat(sidebar): chat file links reuse a single preview tab Edit/write file links from the chat now land in one fixed chat-preview tab: same-type editor opens patch title/path in place, diff or type switches rebuild under the same id, floated previews are patched or re-docked. The reducer path bypasses openTab's per-path dedupe so persistent editor tabs never capture the preview seed. Explorer and editor-menu opens keep one-tab-per-file behavior. --- src/client/chat-preview.ts | 125 ++++++++++++++++ src/client/intercept.tsx | 64 +++++---- tests/chat-preview.spec.ts | 232 ++++++++++++++++++++++++++++++ tests/openpath-intercept.spec.ts | 45 ++++-- tests/turn-tail-intercept.spec.ts | 22 ++- 5 files changed, 439 insertions(+), 49 deletions(-) create mode 100644 src/client/chat-preview.ts create mode 100644 tests/chat-preview.spec.ts diff --git a/src/client/chat-preview.ts b/src/client/chat-preview.ts new file mode 100644 index 000000000..6de8a9805 --- /dev/null +++ b/src/client/chat-preview.ts @@ -0,0 +1,125 @@ +/** + * Chat preview tab (VSCode preview semantics): a single reusable tab for + * file opens triggered from the chat. The chat's edit/write links always + * reuse one tab id (`chat-preview`); switching files replaces its content + * in place. FileTree / editor menu opens keep per-file tabs. + * @module dsh-better-sidebar/client/chat-preview + */ +import { + activateTab, + allLeaves, + closeFloatByTab, + closeTab, + firstLeaf, + floatWithTab, + openTabInActivePane, + patchTab, + raiseFloat, + togglePanel, + type SidebarStore, + type SidebarTab, + type SidebarState, +} from './state.ts' + +/** Fixed id of the single chat preview tab. */ +export const CHAT_PREVIEW_TAB_ID = 'chat-preview' + +/** + * Locate the preview tab across both panel trees and floating windows. + * @param state - per-session sidebar state. + * @returns location descriptor, or null when no preview tab exists. + */ +export function locatePreviewTab( + state: SidebarState, +): { where: 'pane'; paneId: string; tab: SidebarTab } | { where: 'float'; floatId: string; tab: SidebarTab } | null { + for (const leaf of allLeaves(state.splits).concat(allLeaves(state.bottomSplits))) { + const tab = leaf.tabs.find(candidate => candidate.id === CHAT_PREVIEW_TAB_ID) + if (tab !== undefined) return { where: 'pane', paneId: leaf.id, tab } + } + const floated = floatWithTab(state, CHAT_PREVIEW_TAB_ID) + if (floated !== undefined) return { where: 'float', floatId: floated.id, tab: floated.tab } + return null +} + +/** + * Whether a preview tab is currently open in any pane or float. + * @param state - per-session sidebar state. + * @returns true when the preview tab exists. + */ +export function hasPreviewTab(state: SidebarState): boolean { + return locatePreviewTab(state) !== null +} + +/** + * Apply a preview tab to the store, reusing the single fixed id. + * + * Contract: the chat's file opens always land in one tab. Switching files + * replaces its content in place without creating a second tab. The editor + * path update uses `patchTab` so the tab keeps its id and meta while the + * EditorHost reloads on path change; diff tabs are recreated because patch + * cannot change the diff reference. A floating preview stays floating only + * for editor→editor replacements; every other transition closes the float + * and recreates the tab in the right panel. + * + * Panel visibility: when the preview lives in a pane (or does not yet + * exist), the right panel is expanded if collapsed and `activePane` is + * pinned to the right tree's first leaf so the preview lands in sight. + * A floating preview is already in sight and needs no panel change + * (editor→editor raises the float, otherwise the float is closed). + * + * @param store - per-session sidebar store. + * @param tab - preview tab to show. Must carry id `chat-preview`; type is + * `editor` (with path/title) or `diff` (with diff/title). The caller + * constructs it from the probed git status. + */ +export function applyChatPreview(store: SidebarStore, tab: SidebarTab): void { + const snapshot = store.getSnapshot() + const state = snapshot.state + if (state === undefined) return + const located = locatePreviewTab(state) + + // No existing preview: ensure panel visible, pin to right, then land. + if (located === null) { + store.reduce(s => (s.panelOpen ? s : togglePanel(s))) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + store.reduce(s => openTabInActivePane(s, tab)) + return + } + + // Preview is floating. + if (located.where === 'float') { + // Editor → editor keeps the float: patch in place and raise. + if (located.tab.type === 'editor' && tab.type === 'editor') { + store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path })) + store.reduce(s => { + const floated = floatWithTab(s, CHAT_PREVIEW_TAB_ID) + return floated !== undefined ? raiseFloat(s, floated.id) : s + }) + return + } + // Every other transition (diff involved or type swap): close the float + // and recreate in the right panel. + store.reduce(s => closeFloatByTab(s, CHAT_PREVIEW_TAB_ID)) + store.reduce(s => (s.panelOpen ? s : togglePanel(s))) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + store.reduce(s => openTabInActivePane(s, tab)) + return + } + + // Preview is docked in a pane. + const paneId = located.paneId + // Same type editor: patch in place and focus. + if (located.tab.type === 'editor' && tab.type === 'editor') { + store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path })) + store.reduce(s => activateTab(s, paneId, CHAT_PREVIEW_TAB_ID)) + // Ensure visible: expand + pin to right (only when not floating). + store.reduce(s => (s.panelOpen ? s : togglePanel(s))) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + return + } + // Diff → diff, or any type swap: close and recreate (diff cannot be patched). + store.reduce(s => closeTab(s, paneId, CHAT_PREVIEW_TAB_ID)) + store.reduce(s => (s.panelOpen ? s : togglePanel(s))) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + store.reduce(s => openTabInActivePane(s, tab)) +} diff --git a/src/client/intercept.tsx b/src/client/intercept.tsx index df29e7bfd..64ed8b686 100644 --- a/src/client/intercept.tsx +++ b/src/client/intercept.tsx @@ -14,6 +14,7 @@ import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts' import { wrapOpenPath } from './openpath-intercept.ts' import { api } from './api.ts' import { buildEditDiffTab, deriveEditDiffTarget } from './edit-diff.ts' +import { applyChatPreview, CHAT_PREVIEW_TAB_ID } from './chat-preview.ts' import css from './sidebar.module.css' /** @@ -37,15 +38,17 @@ export function openSidebarEditorFile(ctx: Context, store: SidebarStore, session /** * Open a file triggered by the chat's edit-tool path links (via - * `ctx.workspaces.openPath`). When the `editOpensDiff` pref is on (default) - * the file opens as a git worktree diff tab instead of the editor; when off - * — or when the file is not inside a git repository — it falls back to the - * editor. The diff attempt probes the file's owning repository directly - * (`git.status-at`), so edits in external checkouts (outside the session - * workspace but inside an allowed extra root) still surface their diff. - * The probe is async but callers do not await it: a fire-and-forget probe is - * safe because the fallback is still the editor and the workspaces wrapper - * already resolved as success. + * `ctx.workspaces.openPath`). Chat opens always reuse a single preview tab + * (`chat-preview`): switching files replaces its content in place instead of + * creating a new tab. The preview bypasses the editor's per-path dedupe so + * the seed never lands in an existing resident editor tab. When the + * `editOpensDiff` pref is on (default) the preview opens as a git worktree + * diff tab; otherwise it falls back to the editor. The diff probe reaches + * the file's owning repository directly (`git.status-at`). The preview type + * cannot be patched for diff (DiffTab reloads on diff reference), so a diff + * replacement closes and recreates the tab. Panel visibility is ensured for + * pane-hosted previews (expand + pin to the right panel); a floating preview + * stays floating only for editor→editor replacements. * @param ctx - client cordis context. * @param store - per-session sidebar store. * @param sessionId - owning session. @@ -53,32 +56,33 @@ export function openSidebarEditorFile(ctx: Context, store: SidebarStore, session */ export async function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): Promise { const prefs = store.getPrefs() - // Pref off → editor exactly as before. - if (prefs.editOpensDiff === false) { - openSidebarEditorFile(ctx, store, sessionId, path) - return - } - // Diff tab itself disabled → nothing to open as diff, fall back to editor. - if (prefs.tabsEnabled['diff'] === false) { - openSidebarEditorFile(ctx, store, sessionId, path) - return - } const summary = ctx.sessions.list.getSnapshot().byId[sessionId] const cwd = summary?.cwd const absolute = resolveSidebarPath(cwd, path) - try { - const scope = { sessionId, ...(cwd !== undefined ? { cwd } : {}) } as { sessionId: string; cwd?: string } - const status = await api.gitStatusAt(scope, absolute) - const target = deriveEditDiffTarget(absolute, status) - if (target !== null) { - const tab = buildEditDiffTab(target.relative, target.repoRoot, target.untracked) - ctx.get('betterSidebar')?.openTab(tab) - return + let previewTab: import('./state.ts').SidebarTab | null = null + const canProbeDiff = prefs.editOpensDiff !== false && prefs.tabsEnabled['diff'] !== false + if (canProbeDiff) { + try { + const scope = { sessionId, ...(cwd !== undefined ? { cwd } : {}) } as { sessionId: string; cwd?: string } + const status = await api.gitStatusAt(scope, absolute) + const target = deriveEditDiffTarget(absolute, status) + if (target !== null) { + const seed = buildEditDiffTab(target.relative, target.repoRoot, target.untracked) + previewTab = { ...seed, id: CHAT_PREVIEW_TAB_ID, title: seed.title ?? target.relative.split('/').pop() ?? target.relative } as import('./state.ts').SidebarTab + } + } catch { + // Probe failed (network, not a repo, host degraded): fall through to editor preview. } - } catch { - // Probe failed (network, not a repo, host degraded): fall through to editor. } - openSidebarEditorFile(ctx, store, sessionId, path) + if (previewTab === null) { + const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\')) + const title = at === -1 ? absolute : absolute.slice(at + 1) + previewTab = { id: CHAT_PREVIEW_TAB_ID, type: 'editor', title, path: absolute } + } + // Bypass service.openTab's dedupe (editor dedupes by path) and manipulate + // the store directly so the fixed preview id always reuses the same tab. + applyChatPreview(store, previewTab) + void ctx } /** diff --git a/tests/chat-preview.spec.ts b/tests/chat-preview.spec.ts new file mode 100644 index 000000000..1589e9fac --- /dev/null +++ b/tests/chat-preview.spec.ts @@ -0,0 +1,232 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { + createSidebarStore, + allLeaves, + floatTab, + firstLeaf, + openTabInActivePane, + togglePanel, +} from '../src/client/state.ts' +import { CHAT_PREVIEW_TAB_ID, applyChatPreview, locatePreviewTab } from '../src/client/chat-preview.ts' + +// Browser globals for store persist (mirrors service.spec.ts setup) +const g = globalThis as Record +beforeEach(() => { + if (g.window === undefined) { + g.window = { clearTimeout: () => {}, setTimeout: (_fn: () => void) => 0, innerWidth: 1024, innerHeight: 800 } + } + if (g.localStorage === undefined) { + g.localStorage = { getItem: () => null, setItem: () => {} } + } +}) + +describe('chat preview tab (single preview, VSCode semantics)', () => { + const editorPreview = (path: string): import('../src/client/state.ts').SidebarTab => { + const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + const title = at === -1 ? path : path.slice(at + 1) + return { id: CHAT_PREVIEW_TAB_ID, type: 'editor', title, path } + } + const diffPreview = (relative: string, repoRoot = '/repo'): import('../src/client/state.ts').SidebarTab => ({ + id: CHAT_PREVIEW_TAB_ID, + type: 'diff', + title: relative.split('/').pop() ?? relative, + diff: { kind: 'worktree', path: relative, staged: false, untracked: false, repoRoot }, + }) + + const countTabs = (store: ReturnType) => { + const state = store.getSnapshot().state! + return allLeaves(state.splits).concat(allLeaves(state.bottomSplits)).flatMap(l => l.tabs).length + state.floats.length + } + const findPreview = (store: ReturnType) => { + const state = store.getSnapshot().state! + return locatePreviewTab(state) + } + + it('first chat open creates a single editor preview tab pinned to the right panel and expands the panel', () => { + const store = createSidebarStore() + store.setSession('s1') + // Start collapsed (store defaults openByDefault false → panelOpen false in node env). + store.reduce(s => (s.panelOpen ? togglePanel(s) : s)) + expect(store.getSnapshot().state?.panelOpen).toBe(false) + applyChatPreview(store, editorPreview('/repo/a.ts')) + const state = store.getSnapshot().state! + expect(state.panelOpen).toBe(true) + expect(state.activePane).toBe(firstLeaf(state.splits).id) + const preview = findPreview(store) + expect(preview).not.toBeNull() + expect(preview!.tab.type).toBe('editor') + expect(preview!.tab.path).toBe('/repo/a.ts') + // Only the seeded Files home + the preview (2 tabs in right leaf). + expect(countTabs(store)).toBe(2) + }) + + it('second chat open with different file replaces path/title, does not add a tab (editor→editor)', () => { + const store = createSidebarStore() + store.setSession('s1') + applyChatPreview(store, editorPreview('/repo/a.ts')) + const before = countTabs(store) + applyChatPreview(store, editorPreview('/repo/b.ts')) + expect(countTabs(store)).toBe(before) + const preview = findPreview(store)! + expect(preview.tab.type).toBe('editor') + expect(preview.tab.path).toBe('/repo/b.ts') + expect(preview.tab.title).toBe('b.ts') + expect(preview.tab.id).toBe(CHAT_PREVIEW_TAB_ID) + }) + + it('editor→diff closes and recreates with same id but type diff', () => { + const store = createSidebarStore() + store.setSession('s1') + applyChatPreview(store, editorPreview('/repo/a.ts')) + applyChatPreview(store, diffPreview('src/b.ts')) + const preview = findPreview(store)! + expect(preview.tab.id).toBe(CHAT_PREVIEW_TAB_ID) + expect(preview.tab.type).toBe('diff') + expect(preview.tab.diff).toMatchObject({ path: 'src/b.ts' }) + // Still single preview, not two. + expect(countTabs(store)).toBe(2) + // Ensure old editor preview gone: no editor preview path remains. + const state = store.getSnapshot().state! + const allTabs = allLeaves(state.splits).concat(allLeaves(state.bottomSplits)).flatMap(l => l.tabs).concat(state.floats.map(f => f.tab)) + expect(allTabs.filter(t => t.id === CHAT_PREVIEW_TAB_ID && t.type === 'editor')).toHaveLength(0) + }) + + it('diff→diff recreates (diff cannot be patched)', () => { + const store = createSidebarStore() + store.setSession('s1') + applyChatPreview(store, diffPreview('src/a.ts')) + const firstDiff = findPreview(store)!.tab.diff as { path?: string } | undefined + applyChatPreview(store, diffPreview('src/b.ts')) + const second = findPreview(store)! + expect(second.tab.type).toBe('diff') + expect(second.tab.diff).toMatchObject({ path: 'src/b.ts' }) + expect(firstDiff?.path).toBe('src/a.ts') + expect(countTabs(store)).toBe(2) + }) + + it('diff→editor swap recreates as editor', () => { + const store = createSidebarStore() + store.setSession('s1') + applyChatPreview(store, diffPreview('src/a.ts')) + applyChatPreview(store, editorPreview('/repo/c.ts')) + const preview = findPreview(store)! + expect(preview.tab.type).toBe('editor') + expect(preview.tab.path).toBe('/repo/c.ts') + }) + + it('floating preview editor→editor stays floating (patched) and is raised', () => { + const store = createSidebarStore() + store.setSession('s1') + applyChatPreview(store, editorPreview('/repo/a.ts')) + // Float the preview. + const state = store.getSnapshot().state! + const previewId = CHAT_PREVIEW_TAB_ID + store.reduce(s => floatTab(s, previewId, 100, 100)) + expect(store.getSnapshot().state!.floats.some(f => f.tab.id === previewId)).toBe(true) + // Add a second float above it to test raising. + store.reduce(s => openTabInActivePane(s, { id: 'dummy:1', type: 'terminal', title: 'Dummy' })) + store.reduce(s => floatTab(s, 'dummy:1', 200, 200)) + const before = store.getSnapshot().state! + expect(before.floats).toHaveLength(2) + expect(before.floats[0]!.tab.id).toBe(previewId) + // Editor→editor should patch and raise preview to top. + applyChatPreview(store, editorPreview('/repo/b.ts')) + const after = store.getSnapshot().state! + expect(after.floats).toHaveLength(2) + expect(after.floats.at(-1)!.tab.id).toBe(previewId) + expect(after.floats.at(-1)!.tab.path).toBe('/repo/b.ts') + // Panel must NOT have been expanded for floating case (stay collapsed if was collapsed). + // Start collapsed, floating keeps it collapsed. + const collapsedStore = createSidebarStore() + collapsedStore.setSession('s1') + collapsedStore.reduce(s => (s.panelOpen ? togglePanel(s) : s)) + expect(collapsedStore.getSnapshot().state?.panelOpen).toBe(false) + applyChatPreview(collapsedStore, editorPreview('/repo/a.ts')) + collapsedStore.reduce(s => floatTab(s, CHAT_PREVIEW_TAB_ID, 100, 100)) + collapsedStore.reduce(s => (s.panelOpen ? s : s)) // ensure still collapsed before second preview + collapsedStore.reduce(s => ({ ...s, panelOpen: false })) + applyChatPreview(collapsedStore, editorPreview('/repo/b.ts')) + expect(collapsedStore.getSnapshot().state!.panelOpen).toBe(false) + }) + + it('floating preview diff→diff and editor→diff close the float and recreate in panel (expanded)', () => { + const store = createSidebarStore() + store.setSession('s1') + // Start with editor preview floated + applyChatPreview(store, editorPreview('/repo/a.ts')) + store.reduce(s => floatTab(s, CHAT_PREVIEW_TAB_ID, 100, 100)) + expect(store.getSnapshot().state!.floats).toHaveLength(1) + // Collapse panel to test recreation expands it + store.reduce(s => ({ ...s, panelOpen: false })) + // editor→diff should close float and land in panel + applyChatPreview(store, diffPreview('src/x.ts')) + const after = store.getSnapshot().state! + expect(after.floats).toHaveLength(0) + expect(after.panelOpen).toBe(true) + expect(findPreview(store)!.where).toBe('pane') + expect(findPreview(store)!.tab.type).toBe('diff') + + // Diff floated → editor should also close and land in panel + store.reduce(s => floatTab(s, CHAT_PREVIEW_TAB_ID, 100, 100)) + store.reduce(s => ({ ...s, panelOpen: false })) + applyChatPreview(store, editorPreview('/repo/z.ts')) + const after2 = store.getSnapshot().state! + expect(after2.floats).toHaveLength(0) + expect(after2.panelOpen).toBe(true) + expect(findPreview(store)!.tab.type).toBe('editor') + }) + + it('tabs total stays bounded: repeated preview opens never exceed seeded + 1', () => { + const store = createSidebarStore() + store.setSession('s1') + const paths = ['/repo/a.ts', '/repo/b.ts', '/repo/c.ts', '/repo/d.ts', '/repo/e.ts'] + for (const path of paths) applyChatPreview(store, editorPreview(path)) + expect(countTabs(store)).toBe(2) // seeded Files + single preview + // Intermix diffs + applyChatPreview(store, diffPreview('src/a.ts')) + applyChatPreview(store, diffPreview('src/b.ts')) + applyChatPreview(store, editorPreview('/repo/f.ts')) + expect(countTabs(store)).toBe(2) + }) + + it('resident editor tabs (per-path) coexist with preview and are never reused by preview', () => { + const store = createSidebarStore() + store.setSession('s1') + // Create a resident editor tab via direct openTabInActivePane (simulates Files/openSidebarEditorFile) + store.reduce(s => openTabInActivePane(s, { id: 'editor:/repo/resident.ts', type: 'editor', title: 'resident.ts', path: '/repo/resident.ts' })) + const beforeResidentCount = countTabs(store) + expect(beforeResidentCount).toBe(2) // seeded + resident + // Preview opens a different file that would dedupe to resident if using service dedupe + applyChatPreview(store, editorPreview('/repo/resident.ts')) + // Preview must be separate tab with fixed id, not reuse resident + const state = store.getSnapshot().state! + const tabs = allLeaves(state.splits).concat(allLeaves(state.bottomSplits)).flatMap(l => l.tabs) + expect(tabs.some(t => t.id === 'editor:/repo/resident.ts' && t.path === '/repo/resident.ts')).toBe(true) + expect(tabs.some(t => t.id === CHAT_PREVIEW_TAB_ID && t.path === '/repo/resident.ts')).toBe(true) + expect(tabs.filter(t => t.path === '/repo/resident.ts')).toHaveLength(2) + // Total now 3 (seeded + resident + preview) + expect(countTabs(store)).toBe(3) + // Second preview with another file replaces preview, resident untouched + applyChatPreview(store, editorPreview('/repo/other.ts')) + const after = store.getSnapshot().state! + const tabsAfter = allLeaves(after.splits).concat(allLeaves(after.bottomSplits)).flatMap(l => l.tabs) + expect(tabsAfter.some(t => t.id === 'editor:/repo/resident.ts')).toBe(true) + expect(tabsAfter.some(t => t.id === CHAT_PREVIEW_TAB_ID && t.path === '/repo/other.ts')).toBe(true) + expect(countTabs(store)).toBe(3) + }) + + it('preview remains correct when activePane was in bottom panel (pins to right)', () => { + const store = createSidebarStore() + store.setSession('s1') + // Move activePane to bottom panel + const bottomId = (store.getSnapshot().state!.bottomSplits as { id: string }).id + store.reduce(s => ({ ...s, activePane: bottomId })) + store.reduce(s => ({ ...s, panelOpen: false })) + applyChatPreview(store, editorPreview('/repo/a.ts')) + const state = store.getSnapshot().state! + expect(state.panelOpen).toBe(true) + expect(state.activePane).toBe(firstLeaf(state.splits).id) + expect(allLeaves(state.splits).flatMap(l => l.tabs).some(t => t.id === CHAT_PREVIEW_TAB_ID)).toBe(true) + expect(allLeaves(state.bottomSplits).flatMap(l => l.tabs).some(t => t.id === CHAT_PREVIEW_TAB_ID)).toBe(false) + }) +}) diff --git a/tests/openpath-intercept.spec.ts b/tests/openpath-intercept.spec.ts index cc6fec6f1..a3c93fcc4 100644 --- a/tests/openpath-intercept.spec.ts +++ b/tests/openpath-intercept.spec.ts @@ -4,6 +4,14 @@ import { wrapOpenPath, type OpenPathInterceptDeps, type OpenPathService } from ' import { createSidebarStore } from '../src/client/state.ts' import type { Context } from '../src/context-types.ts' +const g = globalThis as Record +if (g.window === undefined) { + g.window = { clearTimeout: () => {}, setTimeout: (_fn: () => void) => 0, innerWidth: 1024, innerHeight: 800 } as unknown as Window +} +if (g.localStorage === undefined) { + g.localStorage = { getItem: () => null, setItem: () => {} } as unknown as Storage +} + describe('open-path interception', () => { /** A minimal fake of the workspaces.openPath service method. */ const service = (): OpenPathService & { calls: string[]; opened: string[] } => { @@ -98,38 +106,43 @@ describe('open-path interception', () => { }) describe('open-path interception wiring', () => { - it('registerOpenPathInterception routes chat opens into the editor tab and restores on dispose', async () => { + it('registerOpenPathInterception routes chat opens into the preview tab and restores on dispose', async () => { // A realistic client-context fake: the sessions list feed (current + cwd), // the workspaces funnel, and the sidebar service the editor goes through. - const opened: Array> = [] const funnel = { openPath: async (): Promise => {} } const ctx = { sessions: { list: { getSnapshot: () => ({ current: 's1', byId: { s1: { cwd: '/w' } } }) }, }, workspaces: funnel, - betterSidebar: { openTab: (seed: unknown) => { opened.push(seed as Record) } }, - get: (name: string) => name === 'betterSidebar' - ? { openTab: (seed: unknown) => { opened.push(seed as Record) } } - : undefined, + betterSidebar: { openTab: () => {} }, + get: (name: string) => name === 'betterSidebar' ? { openTab: () => {} } : undefined, } as unknown as Context const store = createSidebarStore() - // The edit→diff pref defaults on, but this wiring spec pins the legacy - // editor routing (the diff path is async and needs a git probe). Disable - // the diff pref so the open lands synchronously in the editor as before. + store.setSession('s1') + // The edit→diff pref defaults on, but this wiring spec pins the + // preview editor routing (the diff path is async and needs a git probe). + // Disable the diff pref so the open lands synchronously in the preview + // editor as before. store.setPrefs({ ...store.getPrefs(), editOpensDiff: false }) const original = ctx.workspaces.openPath const restore = registerOpenPathInterception(ctx, store) - // Default prefs (with edit diff off): the takeover routes the open into the sidebar editor - // with the session-scoped absolute path (chat already resolved it). + // Default prefs (with edit diff off): the takeover routes the open into the single preview tab + // with the session-scoped absolute path (chat already resolved it). The preview uses the fixed id. await ctx.workspaces.openPath('/w/src/a.ts') - expect(opened).toEqual([{ + // Allow the fire-and-forget preview to settle (no await in the wrapper). + await new Promise(resolve => setTimeout(resolve, 0)) + const state = store.getSnapshot().state! + const preview = state.splits.kind === 'leaf' + ? (state.splits as { tabs: Array<{ id: string; type: string; title: string; path?: string }> }).tabs.find(t => t.id === 'chat-preview') + : undefined + expect(preview).toEqual(expect.objectContaining({ type: 'editor', title: 'a.ts', path: '/w/src/a.ts', - id: 'editor:/w/src/a.ts', - }]) + id: 'chat-preview', + })) // The interceptOpenPath pref off → the original funnel runs untouched. store.setPrefs({ ...store.getPrefs(), interceptOpenPath: false }) @@ -137,7 +150,9 @@ describe('open-path interception wiring', () => { ctx.workspaces.openPath = async (path: string) => { calls.push(path) } await ctx.workspaces.openPath('/w/src/b.ts') expect(calls).toEqual(['/w/src/b.ts']) - expect(opened).toHaveLength(1) + // Preview still single. + const tabsAfter = (store.getSnapshot().state!.splits as { tabs: Array<{ id: string }> }).tabs + expect(tabsAfter.filter(t => t.id === 'chat-preview')).toHaveLength(1) // The editor tab disabled → falls through too (an editor that cannot // open must not swallow opens). diff --git a/tests/turn-tail-intercept.spec.ts b/tests/turn-tail-intercept.spec.ts index 5bbf3bdd2..965804760 100644 --- a/tests/turn-tail-intercept.spec.ts +++ b/tests/turn-tail-intercept.spec.ts @@ -15,6 +15,14 @@ import { createSidebarStore } from '../src/client/state.ts' import { registerTurnTailInterception } from '../src/client/intercept.tsx' import type { Context } from '../src/context-types.ts' +const g = globalThis as Record +if (g.window === undefined) { + g.window = { clearTimeout: () => {}, setTimeout: (_fn: () => void) => 0, innerWidth: 1024, innerHeight: 800 } as unknown as Window +} +if (g.localStorage === undefined) { + g.localStorage = { getItem: () => null, setItem: () => {} } as unknown as Storage +} + interface RegisteredSlot { options: Record component: unknown @@ -166,10 +174,11 @@ describe('turn-tail interception registration (issue #15)', () => { restore() }) - it('wires the openInSidebar and onShowInFolder seats', () => { + it('wires the openInSidebar and onShowInFolder seats', async () => { const fake = fakeSlots(true) const ctx = clientCtx(fake.slots) const store = createSidebarStore() + store.setSession('s1') store.setPrefs({ ...store.getPrefs(), editOpensDiff: false }) const restore = registerTurnTailInterception(ctx, store) const inject = fake.registered[0]!.options.inject as (sessionId: string) => { @@ -177,21 +186,26 @@ describe('turn-tail interception registration (issue #15)', () => { onShowInFolder: (files: readonly string[]) => void } - // The seat hands the session-scoped opener to the chips row. + // The seat hands the session-scoped opener to the chips row — chat opens land in the single preview tab. const seat = inject('s1') expect(seat.openInSidebar).toBeTypeOf('function') seat.openInSidebar('/w/src/a.ts') - expect(ctx.betterSidebar.openTab).toHaveBeenCalledWith({ + await new Promise(resolve => setTimeout(resolve, 0)) + const state = store.getSnapshot().state! + const preview = (state.splits as { tabs: Array<{ id: string }> }).tabs.find(t => t.id === 'chat-preview') + expect(preview).toBeDefined() + expect(preview).toMatchObject({ type: 'editor', title: 'a.ts', path: '/w/src/a.ts', - id: 'editor:/w/src/a.ts', + id: 'chat-preview', }) // The show-in-folder seat reveals the produced files in the files window // (the editor home tab) — the panel expands and the rows highlight. expect(seat.onShowInFolder).toBeTypeOf('function') seat.onShowInFolder(['/w/src/a.ts']) + // Reveal uses the editor home tab (Files), not the preview. expect(ctx.betterSidebar.openTab).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'editor', })) From b1156f37ea8d29cd64feb2ae848385d9c50aad99 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Thu, 27 Aug 2026 11:32:29 +0800 Subject: [PATCH 05/11] fix(sidebar): auto-open task management for every new subagent detectNewDirectSubagent fired only on the 0-to-1 transition, so sessions that ever had a subagent never auto-opened again. It now diffs direct subagent ids per snapshot (same new-id semantics as detectNewJob); the debounce window merges concurrent triggers instead of dropping them. --- src/client/Sidebar.tsx | 27 ++++++++++++++++++++++- src/client/subagent-detect.ts | 41 ++++++++++++++++++++++++++++++----- tests/subagent-detect.spec.ts | 9 +++++--- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index 97c8160b0..5563e36fc 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -551,6 +551,14 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { * a new subagent and pop this page on every thread creation. The timer * re-evaluates the ORIGINAL baseline against the live snapshot; by then * the title filter (isSideThreadSummary) sees the settled label. + * + * Multiple new subagents that appear within the debounce window are merged + * into one trigger: a pending timer is reset with the ORIGINAL baseline + * kept, so any new id that arrived while waiting is still detected on the + * re-evaluation (per-id diff semantics, matching detectNewJob). The + * alternative discard would miss a second new id that arrived during the + * wait. This merge choice preserves the title-debounce while surfacing + * every burst of subagents as a single auto-open. */ const listBaselineRef = useRef(undefined) const autoOpenPendingRef = useRef<{ baseline: SidebarSessionList; timer: number } | null>(null) @@ -558,8 +566,25 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) { const prev = listBaselineRef.current listBaselineRef.current = sessionList if (sessionId === undefined || prev === undefined) return - if (autoOpenPendingRef.current !== null) return if (!detectNewDirectSubagent(prev, sessionList, sessionId)) return + // Debounce-window merge: if a timer is already armed, keep the ORIGINAL + // baseline (so all ids since the first trigger are considered) and reset + // the window instead of discarding the new trigger. + if (autoOpenPendingRef.current !== null) { + const baseline = autoOpenPendingRef.current.baseline + window.clearTimeout(autoOpenPendingRef.current.timer) + const timer = window.setTimeout(() => { + autoOpenPendingRef.current = null + if (!detectNewDirectSubagent(baseline, ctx.sessions.list.getSnapshot(), sessionId)) return + if (!store.getPrefs().autoOpenSubagent) return + if (ctx.get('betterSidebar')?.isTabEnabled('subagent') === false) return + store.reduce(s => s.panelOpen ? s : togglePanel(s)) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + ctx.get('betterSidebar')?.openTab({ type: 'subagent', title: t('subagent') }) + }, AUTO_OPEN_DEBOUNCE_MS) + autoOpenPendingRef.current = { baseline, timer } + return + } const baseline = prev const timer = window.setTimeout(() => { autoOpenPendingRef.current = null diff --git a/src/client/subagent-detect.ts b/src/client/subagent-detect.ts index f45b3ad54..f48cabb78 100644 --- a/src/client/subagent-detect.ts +++ b/src/client/subagent-detect.ts @@ -40,6 +40,26 @@ export function directSubagentCount( return count } +/** + * Collect the ids of direct subagent children of one session, excluding Side + * Chat threads. Stable under the same `isSideThreadSummary` filter as the + * count helper. + * @param byId - session map from the list snapshot. + * @param sessionId - parent session whose children are collected. + * @returns set of direct subagent session ids. + */ +export function directSubagentIds( + byId: SidebarSessionList['byId'], + sessionId: string, +): Set { + const ids = new Set() + for (const summary of Object.values(byId)) { + if (summary.origin === 'subagent' && summary.parentId === sessionId + && !isSideThreadSummary(summary)) ids.add(summary.id) + } + return ids +} + /** * The main agent of the current session's tree: walk the durable parent * chain upward until the first non-subagent session. The Subagent page shows @@ -89,17 +109,28 @@ export function collectBranchIds( /** * Whether a new direct subagent appeared under `sessionId` between two - * consecutive list snapshots (the count crossed 0 → >0). Switching to a - * session that already has subagents yields `false` (its baseline starts at - * the current count), so the auto-open never fights an existing layout. + * consecutive list snapshots. Triggers when any direct subagent id present + * in `next` was absent in `prev` (per-id diff, matching `detectNewJob`). + * Side Chat threads (`Side: ` prefix) are excluded, and switching to a + * session that already has subagents yields false until a genuinely new id + * arrives. + * @param prev - previous list snapshot. + * @param next - next list snapshot. + * @param sessionId - parent session to inspect. + * @returns true when a new direct subagent id appeared. */ export function detectNewDirectSubagent( prev: SidebarSessionList, next: SidebarSessionList, sessionId: string, ): boolean { - return directSubagentCount(prev.byId, sessionId) === 0 - && directSubagentCount(next.byId, sessionId) > 0 + const prevIds = directSubagentIds(prev.byId, sessionId) + for (const summary of Object.values(next.byId)) { + if (summary.origin !== 'subagent' || summary.parentId !== sessionId) continue + if (isSideThreadSummary(summary)) continue + if (!prevIds.has(summary.id)) return true + } + return false } /** Descendant totals of one session through an uninterrupted subagent-origin chain. */ diff --git a/tests/subagent-detect.spec.ts b/tests/subagent-detect.spec.ts index 2fe4e069b..9e5db5d43 100644 --- a/tests/subagent-detect.spec.ts +++ b/tests/subagent-detect.spec.ts @@ -33,15 +33,18 @@ describe('subagent detection over the sessions list feed', () => { expect(directSubagentCount(snapshot.byId, 'p1-nobody')).toBe(0) }) - it('fires only on the 0 → N transition of the current session', () => { + it('fires on any new direct subagent id (per-id diff, not 0 → N)', () => { const empty = list('p1', []) const one = list('p1', ['c1']) const two = list('p1', ['c1', 'c2']) expect(detectNewDirectSubagent(empty, empty, 'p1')).toBe(false) expect(detectNewDirectSubagent(empty, one, 'p1')).toBe(true) - // Already-present children never re-trigger (session switch, reload). - expect(detectNewDirectSubagent(one, two, 'p1')).toBe(false) + // Any new id triggers, even when some already existed (1 → 2 with c2 new). + expect(detectNewDirectSubagent(one, two, 'p1')).toBe(true) + // Removing a child does not trigger (no new id). expect(detectNewDirectSubagent(two, one, 'p1')).toBe(false) + // Same ids, different order, no new id. + expect(detectNewDirectSubagent(one, list('p1', ['c1']), 'p1')).toBe(false) // A child arriving under ANOTHER session does not trigger this one. expect(detectNewDirectSubagent(empty, list('p2', ['x']), 'p1')).toBe(false) }) From d2f298538d9d812eb2a73b216ee32ad54da5c765 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Sat, 29 Aug 2026 08:28:46 +0800 Subject: [PATCH 06/11] feat: enhance diff editing and tab handling --- src/client/DiffTab.tsx | 2 +- src/client/api.ts | 9 ++-- src/client/builtins/tabs.tsx | 13 ++++- src/client/edit-diff.ts | 33 +++++++++++++ src/client/intercept.tsx | 22 ++++++++- src/client/locales.ts | 4 +- src/client/state.ts | 2 +- src/git.ts | 35 +++++++++++++- src/index.ts | 92 +++++++++++++++++++++++++----------- 9 files changed, 174 insertions(+), 38 deletions(-) diff --git a/src/client/DiffTab.tsx b/src/client/DiffTab.tsx index 8aaef0474..fa872aac8 100644 --- a/src/client/DiffTab.tsx +++ b/src/client/DiffTab.tsx @@ -41,7 +41,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif const load = async (): Promise => { try { if (diff.kind === 'commit') { - const result = await api.gitCommitDiff(scope, diff.hashFull, diff.worktree) + const result = await api.gitCommitDiff(scope, diff.hashFull, diff.worktree, diff.kind === 'commit' ? diff.path : undefined) if (!cancelled) setData({ diff: result.diff }) return } diff --git a/src/client/api.ts b/src/client/api.ts index 50d8febb0..11a925f3a 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -241,9 +241,12 @@ export const api = { ...(count !== undefined ? { count } : {}), ...(skip !== undefined ? { skip } : {}), }), signal), - /** Full patch text of one commit (diff display for the history rows). */ - gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, signal?: AbortSignal) => - call<{ diff: string }>('git.commit-diff', gitPayload(scope, worktree, { hash }), signal), + /** Full patch text of one commit (diff display for the history rows). When `path` is given the diff is limited to that file. */ + gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, path?: string, signal?: AbortSignal) => + call<{ diff: string }>('git.commit-diff', gitPayload(scope, worktree, { ...(path !== undefined ? { path } : {}), hash }), signal), + /** The most recent commit that touched `path` (file-limited last-commit probe for the edit→commit fallback). */ + gitLastCommitAt: (scope: SessionScope, path: string, signal?: AbortSignal) => + call<{ commit: { hash: string; hashFull: string; subject: string; repoRoot: string } | null }>('git.last-commit-at', scopePayload(scope, { path }), signal), /** Discard the worktree changes of one file (the index is untouched). */ gitDiscard: (scope: SessionScope, path: string, worktree?: string) => call<{ ok: true }>('git.discard', gitPayload(scope, worktree, { path })), diff --git a/src/client/builtins/tabs.tsx b/src/client/builtins/tabs.tsx index 90b140f98..edc366ed4 100644 --- a/src/client/builtins/tabs.tsx +++ b/src/client/builtins/tabs.tsx @@ -69,6 +69,17 @@ function terminalUuid(): string { return `t${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` } +/** + * A client-side uuid safe on plain-HTTP LAN pages: crypto.randomUUID is a + * secure-context API, so fall back to a time/random id when absent. + */ +function clientUuid(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}` +} + /** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */ function uiTerminalCount(state: SidebarState): number { return allLeaves(state.splits) @@ -198,7 +209,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read } return { tab: { - id: `sidechat:new-${crypto.randomUUID()}`, + id: `sidechat:new-${clientUuid()}`, type: 'sidechat', title: t('sideChatUntitled'), meta: { autoCreate: true }, diff --git a/src/client/edit-diff.ts b/src/client/edit-diff.ts index 95ddc5f6c..4db2e73c6 100644 --- a/src/client/edit-diff.ts +++ b/src/client/edit-diff.ts @@ -69,3 +69,36 @@ export function buildEditDiffTab(relative: string, repoRoot: string, untracked: diff: { kind: 'worktree', path: relative, staged: false, untracked, repoRoot }, } } + +/** + * Build the commit-diff tab seed for the edit→commit fallback. When the file + * has no unstaged change, the most recent commit that touched it is shown + * instead, limited to that single file. + * + * @param input.relative - Repo-root-relative path of the edited file. + * @param input.repoRoot - Absolute repository root that owns the file. + * @param input.hash - Short hash of the last touching commit. + * @param input.hashFull - Full hash of the last touching commit. + * @param input.subject - Subject line of the last touching commit. + * @returns The openTab seed (id is overwritten by the caller to `chat-preview`). + */ +export function buildCommitDiffTab(input: { + relative: string + repoRoot: string + hash: string + hashFull: string + subject: string +}): OpenTabSeed { + return { + type: 'diff', + title: baseName(input.relative), + diff: { + kind: 'commit', + hash: input.hash, + hashFull: input.hashFull, + subject: input.subject, + path: input.relative, + repoRoot: input.repoRoot, + }, + } +} diff --git a/src/client/intercept.tsx b/src/client/intercept.tsx index 64ed8b686..099f970e2 100644 --- a/src/client/intercept.tsx +++ b/src/client/intercept.tsx @@ -13,7 +13,8 @@ import { t } from './locales.ts' import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts' import { wrapOpenPath } from './openpath-intercept.ts' import { api } from './api.ts' -import { buildEditDiffTab, deriveEditDiffTarget } from './edit-diff.ts' +import { buildCommitDiffTab, buildEditDiffTab, deriveEditDiffTarget } from './edit-diff.ts' +import { relativeTo } from './paths.ts' import { applyChatPreview, CHAT_PREVIEW_TAB_ID } from './chat-preview.ts' import css from './sidebar.module.css' @@ -69,6 +70,25 @@ export async function openSidebarFile(ctx: Context, store: SidebarStore, session if (target !== null) { const seed = buildEditDiffTab(target.relative, target.repoRoot, target.untracked) previewTab = { ...seed, id: CHAT_PREVIEW_TAB_ID, title: seed.title ?? target.relative.split('/').pop() ?? target.relative } as import('./state.ts').SidebarTab + } else { + try { + const last = await api.gitLastCommitAt(scope, absolute) + if (last.commit !== null) { + const rel = relativeTo(last.commit.repoRoot, absolute) + if (rel !== '.' && rel !== absolute) { + const seed = buildCommitDiffTab({ + relative: rel, + repoRoot: last.commit.repoRoot, + hash: last.commit.hash, + hashFull: last.commit.hashFull, + subject: last.commit.subject, + }) + previewTab = { ...seed, id: CHAT_PREVIEW_TAB_ID, title: seed.title ?? rel.split('/').pop() ?? rel } as import('./state.ts').SidebarTab + } + } + } catch { + // Last-commit probe failed (network, not a repo, fence): fall through to editor. + } } } catch { // Probe failed (network, not a repo, host degraded): fall through to editor preview. diff --git a/src/client/locales.ts b/src/client/locales.ts index 032f2ff33..8140fe088 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -197,7 +197,7 @@ export const zh = { settingsOpenPathTitle: '聊天区文件在侧边栏打开', settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用', settingsEditDiffTitle: '编辑类文件以 diff 视图打开', - settingsEditDiffDesc: '开启后,点击编辑工具的文件链接时在侧边栏打开 git 工作区 diff 视图,而不是直接打开文件;非 git 仓库自动回退为文件打开', + settingsEditDiffDesc: '开启后,点击编辑工具的文件链接时在侧边栏打开 git 工作区 diff 视图,而不是直接打开文件;无未提交改动时显示该文件最近提交的 diff,非 git 仓库自动回退为文件打开', settingsOpenToolsTitle: '为模型注入侧边栏打开工具', settingsOpenToolsDesc: '开启后,模型可通过 sidebar_open 工具在侧边栏主动打开文件、文件夹和 HTTP(S) 网页(默认关闭)', settingsTitleBarTitle: '位置兼容模式', @@ -543,7 +543,7 @@ export const en: Record = { settingsOpenPathTitle: 'Open chat files in the sidebar', settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app', settingsEditDiffTitle: 'Open edited files as diff', - settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; non-git workspaces fall back to the file', + settingsEditDiffDesc: 'When on, clicking an edit-tool file link opens the git worktree diff view in the sidebar instead of the file editor; when there is no unstaged change it shows the diff of the last commit that touched the file, and non-git workspaces fall back to the file', settingsOpenToolsTitle: 'Inject the sidebar-open tool for the model', settingsOpenToolsDesc: 'When enabled, the model can actively open files, folders, and HTTP(S) pages in the sidebar through the sidebar_open tool (off by default)', settingsTitleBarTitle: 'Position compatibility mode', diff --git a/src/client/state.ts b/src/client/state.ts index f596eec62..60b9d38bf 100644 --- a/src/client/state.ts +++ b/src/client/state.ts @@ -23,7 +23,7 @@ export type TabType = string /** What a diff tab shows: a worktree/index change of one path, or one commit's full patch. */ export type SidebarDiffRef = | { kind: 'worktree'; path: string; staged: boolean; untracked?: boolean; worktree?: string; repoRoot?: string } - | { kind: 'commit'; hash: string; hashFull: string; subject: string; worktree?: string; repoRoot?: string } + | { kind: 'commit'; hash: string; hashFull: string; subject: string; worktree?: string; repoRoot?: string; /** When present, the commit diff is limited to this file (repoRoot-relative). */ path?: string } /** One open tab. `path` carries the file (editor) or is absent (git/terminal); * `diff` carries the change a diff tab shows; `meta` (v0.12.0+) carries diff --git a/src/git.ts b/src/git.ts index 6cabcc6dc..eb6470eaf 100644 --- a/src/git.ts +++ b/src/git.ts @@ -454,8 +454,39 @@ export async function show(cwd: string, rev: string, path: string, selected?: st /** Full patch text of one commit (`git show` with the commit header suppressed). * Merge commits show their diff against the first parent (`-m --first-parent` * is a no-op for regular commits), so a history click always has content. */ -export async function commitDiff(cwd: string, hash: string, selected?: string): Promise { - return runGit(await repoRoot(cwd, selected), ['show', '--no-ext-diff', '--no-color', '--format=', '-m', '--first-parent', hash]) +export async function commitDiff(cwd: string, hash: string, selected?: string, path?: string): Promise { + const args = ['show', '--no-ext-diff', '--no-color', '--format=', '-m', '--first-parent', hash] + if (path !== undefined) args.push('--', path) + return runGit(await repoRoot(cwd, selected), args) +} + +/** + * The most recent commit that touched `relativePath` inside `root`. + * + * Runs `git -C log -1 --pretty=format:%h%x1f%H%x1f%s -- ` + * (the `--` separator prevents option injection when the path starts with `-`). + * Returns the short hash, full hash, and subject of that commit, or undefined + * when the repository has no commit touching the path or the git probe fails. + * + * @param root - Absolute repository root (already validated via `repoRootOf`). + * @param relativePath - Repository-root-relative path with `/` separators. + * @returns The most recent touching commit, or undefined when none exists. + */ +export async function lastCommitTouching( + root: string, + relativePath: string, +): Promise<{ hash: string; hashFull: string; subject: string } | undefined> { + try { + const output = await runGit(root, ['log', '-1', '--pretty=format:%h%x1f%H%x1f%s', '--', relativePath]) + const trimmed = output.trim() + if (trimmed === '') return undefined + const [hash, hashFull, subject] = trimmed.split('\x1f') + if (hash === undefined || hashFull === undefined || subject === undefined) return undefined + if (hash === '' || hashFull === '') return undefined + return { hash, hashFull, subject } + } catch { + return undefined + } } /** Discard the worktree changes of one path (`git checkout -- `; the index is untouched). */ diff --git a/src/index.ts b/src/index.ts index 317c32c93..b7bf1e497 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ * processes are keyed by session. */ import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises' -import { basename, dirname, extname, isAbsolute, join } from 'node:path' +import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path' import type { IncomingMessage } from 'node:http' import type { Duplex } from 'node:stream' import { WebSocket, WebSocketServer } from 'ws' @@ -282,6 +282,30 @@ function buildApi( const requested = typeof record?.worktree === 'string' && record.worktree !== '' ? record.worktree : undefined return { sessionId: base.sessionId, cwd: await git.resolveWorktree(base.cwd, requested) } } + /** + * Resolve an externally-selected repository that is not in the discovered + * list but still inside the workspace fence. When `repoRoot` names an + * external checkout, the fence is checked via `ensureWorkspacePath` and the + * canonical root is resolved via `repoRootOf`; callers then run git + * directly against that root instead of silently falling back to the + * session's first discovered repository. Returns the canonical external + * root when the fence passes, or undefined when the selection is known or + * absent (caller uses the discovered-root path). + */ + const resolveDiffRepo = async (cwd: string, payload: unknown): Promise => { + const repoRoot = selectedRepoOf(payload) + if (repoRoot === undefined) return undefined + const roots = await git.repoRoots(cwd) + const isKnown = roots.some(root => git.pathIdentity(root) === git.pathIdentity(repoRoot)) + if (isKnown) return undefined + await ensureWorkspacePath(cwd, repoRoot, resolved.extraRoots) + const actual = await git.repoRootOf(repoRoot) + if (actual === undefined) { + throw new git.GitCommandError(`not a git repository: ${repoRoot}`, 'not-repo', 'rev-parse') + } + return actual + } + // Background jobs: the LIST rides the harness's `session/jobs` push // mirror, so these routes only replay output the model has read (from the // session's own event log — no DSH source is touched, the model's @@ -362,36 +386,31 @@ function buildApi( 'git.diff': async (payload) => { const { cwd } = await gitCwdOf(payload) const record = payload as { path?: unknown; staged?: unknown } - const repoRoot = selectedRepoOf(payload) - const hasPath = record.path !== undefined - const rawPath = hasPath ? requireString(payload, 'path') : undefined - if (repoRoot !== undefined) { - const roots = await git.repoRoots(cwd) - const isKnown = roots.some(root => git.pathIdentity(root) === git.pathIdentity(repoRoot)) - if (!isKnown) { - await ensureWorkspacePath(cwd, repoRoot, resolved.extraRoots) - const actual = await git.repoRootOf(repoRoot) - if (actual === undefined) { - throw new git.GitCommandError(`not a git repository: ${repoRoot}`, 'not-repo', 'rev-parse') - } - let path: string | undefined - if (rawPath !== undefined) { - if (isAbsolute(rawPath)) { - path = requireAbsolute(rawPath) - } else { - // Attempts to resolve relative paths against the external repo root - // directly, instead of the session cwd, so a file like - // "src/a.ts" inside the external checkout does not silently fall - // back to the session's first discovered root. - path = requireAbsolute(join(actual, rawPath)) - } - } - return { diff: await git.diff(actual, path, record.staged === true) } + const rawPath = record.path !== undefined ? requireString(payload, 'path') : undefined + const actual = await resolveDiffRepo(cwd, payload) + if (actual !== undefined) { + let path: string | undefined + if (rawPath !== undefined) { + path = isAbsolute(rawPath) ? requireAbsolute(rawPath) : requireAbsolute(join(actual, rawPath)) } + return { diff: await git.diff(actual, path, record.staged === true) } } + const repoRoot = selectedRepoOf(payload) const path = rawPath === undefined ? undefined : await resolveGitPath(cwd, rawPath, repoRoot) return { diff: await git.diff(cwd, path, record.staged === true, repoRoot) } }, + 'git.last-commit-at': async (payload) => { + const { cwd } = cwdOf(payload) + const raw = requireString(payload, 'path') + const absolute = await ensureWorkspacePath(cwd, raw, resolved.extraRoots) + const root = await git.repoRootOf(absolute) + if (root === undefined) return { commit: null } + const rel = relative(root, absolute).replace(/\\/g, '/') + if (rel === '' || rel === '.' ) return { commit: null } + const commit = await git.lastCommitTouching(root, rel) + if (commit === undefined) return { commit: null } + return { commit: { ...commit, repoRoot: root } } + }, 'git.stage': async (payload) => { const { cwd } = await gitCwdOf(payload) const record = payload as { path?: unknown } @@ -434,7 +453,26 @@ function buildApi( }, 'git.commit-diff': async (payload) => { const { cwd } = await gitCwdOf(payload) - return { diff: await git.commitDiff(cwd, requireString(payload, 'hash'), selectedRepoOf(payload)) } + const hash = requireString(payload, 'hash') + const record = payload as { path?: unknown } + const rawPath = typeof record.path === 'string' ? record.path : undefined + const actual = await resolveDiffRepo(cwd, payload) + if (actual !== undefined) { + let path: string | undefined + if (rawPath !== undefined) { + path = isAbsolute(rawPath) ? requireAbsolute(rawPath) : requireAbsolute(join(actual, rawPath)) + } + return { diff: await git.commitDiff(actual, hash, undefined, path) } + } + const repoRoot = selectedRepoOf(payload) + const path = rawPath === undefined ? undefined : await resolveGitPath(cwd, rawPath, repoRoot) + // `path` is already resolved to an absolute workspace path; pass it through + // so `commitDiff` appends `-- ` with option-injection protection. + // For the non-external case `resolveGitPath` collapses repo-relative + // names (e.g. `src/a.ts`) against the selected repository root, so the + // git command still receives a concrete file filter. + const commitPath = rawPath === undefined ? undefined : path + return { diff: await git.commitDiff(cwd, hash, repoRoot, commitPath) } }, 'git.discard': async (payload) => { const { cwd } = await gitCwdOf(payload) From 3b21d824be2372fa668893d7b3e4d80ed2c7cec4 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Mon, 31 Aug 2026 10:45:43 +0800 Subject: [PATCH 07/11] chore: update package.json and add bun.lock --- bun.lock | 1129 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- 2 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 bun.lock diff --git a/bun.lock b/bun.lock new file mode 100644 index 000000000..1aa368ce4 --- /dev/null +++ b/bun.lock @@ -0,0 +1,1129 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "dsh-better-sidebar", + "dependencies": { + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-cpp": "^6.0.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.12", + "@codemirror/lang-java": "^6.0.2", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.2", + "@codemirror/lang-php": "^6.0.2", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/lang-vue": "^0.1.3", + "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "^6.12.4", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "@lezer/highlight": "^1.2.3", + "clsx": "^2.1.1", + "dompurify": "^3.4.14", + "mermaid": "^11.16.1", + "node-pty": "^1.1.0", + "react-icons": "5.7.0", + "rxjs": "^7.8.2", + "schemastery": "^3.18.0", + "ws": "^8.18.0", + }, + "devDependencies": { + "@cordisjs/plugin-loader": "^1.0.0-rc.5", + "@deepseek-ai/dsh-agent": "0.1.2-alpha.2", + "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2", + "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2", + "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", + "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", + "@deepseek-ai/dsh-session": "0.1.2-alpha.2", + "@deepseek-ai/dsh-settings": "0.1.2-alpha.2", + "@deepseek-ai/dsh-subagent": "0.1.2-alpha.2", + "@deepseek-ai/dsh-tools": "0.1.2-alpha.2", + "@huanlin/dsh-plugin-better-locale": "^0.1.0", + "@playwright/test": "^1.62.1", + "@types/node": "^24.0.0", + "@types/react": "~18.3.1", + "@types/react-dom": "~18.3.1", + "@types/ws": "^8.5.10", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^5.5.0", + "cordis": "^4.0.0-rc.8", + "jsdom": "^29.1.1", + "lightningcss": "^1.32.0", + "react": "^18.2.0", + "react-dom": "18.2.0", + "tsdown": "^0.22.2", + "typescript": "^5.6.0", + "unrun": "^0.2.39", + "vitest": "^4.1.8", + }, + "peerDependencies": { + "@deepseek-ai/cordis": "^4.0.1", + "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-session": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-settings": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-subagent": "^0.1.2-alpha.2", + "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2", + "@huanlin/dsh-plugin-better-locale": "^0.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + }, + "optionalPeers": [ + "@huanlin/dsh-plugin-better-locale", + ], + }, + }, + "packages": { + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "1.8.0", "tinyexec": "1.3.0" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "1.0.1", "@csstools/css-calc": "3.3.0", "@csstools/css-color-parser": "4.2.0", "@csstools/css-parser-algorithms": "4.0.0", "@csstools/css-tokenizer": "4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "1.0.1", "@asamuzakjp/nwsapi": "2.3.9", "bidi-js": "1.0.3", "css-tree": "3.2.1", "is-potential-custom-element-name": "1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], + + "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], + + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", { "dependencies": { "css-tree": "3.2.1" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@chevrotain/types": ["@chevrotain/types@11.1.2", "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="], + + "@codemirror/commands": ["@codemirror/commands@6.11.0", "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.11.0.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2" } }, "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA=="], + + "@codemirror/lang-cpp": ["@codemirror/lang-cpp@6.0.3", "https://registry.npmmirror.com/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@lezer/cpp": "1.1.6" } }, "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/css": "1.3.6" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "https://registry.npmmirror.com/@codemirror/lang-go/-/lang-go-6.0.1.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/go": "1.0.1" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.12", "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.12.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/lang-css": "6.3.1", "@codemirror/lang-javascript": "6.2.5", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2", "@lezer/css": "1.3.6", "@lezer/html": "1.3.13" } }, "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w=="], + + "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.2.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@lezer/java": "1.1.3" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/lint": "6.9.7", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2", "@lezer/javascript": "1.5.4" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], + + "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "https://registry.npmmirror.com/@codemirror/lang-json/-/lang-json-6.0.2.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@lezer/json": "1.0.3" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="], + + "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.2", "https://registry.npmmirror.com/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/lang-html": "6.4.12", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2", "@lezer/markdown": "1.7.2" } }, "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw=="], + + "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "https://registry.npmmirror.com/@codemirror/lang-php/-/lang-php-6.0.2.tgz", { "dependencies": { "@codemirror/lang-html": "6.4.12", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/php": "1.0.5" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="], + + "@codemirror/lang-python": ["@codemirror/lang-python@6.2.1", "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.2.1.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/python": "1.1.19" } }, "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw=="], + + "@codemirror/lang-rust": ["@codemirror/lang-rust@6.0.2", "https://registry.npmmirror.com/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", { "dependencies": { "@codemirror/language": "6.12.4", "@lezer/rust": "1.0.2" } }, "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA=="], + + "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="], + + "@codemirror/lang-vue": ["@codemirror/lang-vue@0.1.3", "https://registry.npmmirror.com/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", { "dependencies": { "@codemirror/lang-html": "6.4.12", "@codemirror/lang-javascript": "6.2.5", "@codemirror/language": "6.12.4", "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug=="], + + "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "https://registry.npmmirror.com/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2", "@lezer/xml": "1.0.6" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="], + + "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.3", "https://registry.npmmirror.com/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", { "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/language": "6.12.4", "@codemirror/state": "6.7.1", "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10", "@lezer/yaml": "1.0.4" } }, "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ=="], + + "@codemirror/language": ["@codemirror/language@6.12.4", "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.4.tgz", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10", "style-mod": "4.1.3" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="], + + "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.3", "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", { "dependencies": { "@codemirror/language": "6.12.4" } }, "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.7", "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.9.7.tgz", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "crelt": "1.0.7" } }, "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg=="], + + "@codemirror/search": ["@codemirror/search@6.7.1", "https://registry.npmmirror.com/@codemirror/search/-/search-6.7.1.tgz", { "dependencies": { "@codemirror/state": "6.7.1", "@codemirror/view": "6.43.9", "crelt": "1.0.7" } }, "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA=="], + + "@codemirror/state": ["@codemirror/state@6.7.1", "https://registry.npmmirror.com/@codemirror/state/-/state-6.7.1.tgz", { "dependencies": { "@marijn/find-cluster-break": "1.0.3" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="], + + "@codemirror/view": ["@codemirror/view@6.43.9", "https://registry.npmmirror.com/@codemirror/view/-/view-6.43.9.tgz", { "dependencies": { "@codemirror/state": "6.7.1", "crelt": "1.0.7", "style-mod": "4.1.3", "w3c-keyname": "2.2.8" } }, "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw=="], + + "@cordisjs/plugin-loader": ["@cordisjs/plugin-loader@1.0.0-rc.5", "https://registry.npmmirror.com/@cordisjs/plugin-loader/-/plugin-loader-1.0.0-rc.5.tgz", { "dependencies": { "cosmokit": "1.8.1" }, "peerDependencies": { "cordis": "4.0.0-rc.8" } }, "sha512-084Wn2SzkFinbaASTq8blHOUqQt/oxZfX6gnrt0lnJ1CrystulFLL1+XVgF4o7lUMN9bHn4cfT1pMtkHprCtHw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.3.0.tgz", { "peerDependencies": { "@csstools/css-parser-algorithms": "4.0.0", "@csstools/css-tokenizer": "4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.0", "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", { "dependencies": { "@csstools/color-helpers": "6.1.1", "@csstools/css-calc": "3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "4.0.0", "@csstools/css-tokenizer": "4.0.0" } }, "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", { "peerDependencies": { "@csstools/css-tokenizer": "4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.8", "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", { "optionalDependencies": { "css-tree": "3.2.1" } }, "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + + "@deepseek-ai/cordis": ["@deepseek-ai/cordis@4.0.1", "https://registry.npmmirror.com/@deepseek-ai/cordis/-/cordis-4.0.1.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.2", "@standard-schema/spec": "1.1.0" }, "optionalDependencies": { "@deepseek-ai/cordis-plugin-include": "1.0.6", "@deepseek-ai/cordis-plugin-loader": "1.0.2" }, "bin": { "cordis": "bin.js" } }, "sha512-YBdskTU2Po1kru3GgcUWUbkTsPMA9LkSQDAY8rBkFJeajdgcQad3QPJZE26JyK99Xb6HaASvoXg2DSUTeN/0Nw=="], + + "@deepseek-ai/cordis-plugin-include": ["@deepseek-ai/cordis-plugin-include@1.0.6", "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-include/-/cordis-plugin-include-1.0.6.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.2", "js-yaml": "4.3.1" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/cordis-plugin-loader": "1.0.2" } }, "sha512-i1VXrZCbv6tk/iUgedCNjrxxArbWT3IvRZGB5sdqJ3ectnihivXXQbRZ8JJ73DSmAPvlMGmrbtjFAfm10yvXRg=="], + + "@deepseek-ai/cordis-plugin-loader": ["@deepseek-ai/cordis-plugin-loader@1.0.2", "https://registry.npmmirror.com/@deepseek-ai/cordis-plugin-loader/-/cordis-plugin-loader-1.0.2.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-RIW9hoVyhYDWdCI9BsvtZccPde1ECLC4OAxupwowGTak78vwVTVdb3HezTSOK1Y1/Ax3Ru0LA1pYOB04CnTxIQ=="], + + "@deepseek-ai/cosmokit": ["@deepseek-ai/cosmokit@1.8.2", "https://registry.npmmirror.com/@deepseek-ai/cosmokit/-/cosmokit-1.8.2.tgz", {}, "sha512-muBOKtSrUKU5m/xpq8ZXWL6hQ/jgd4PhU2PqH97bcxIiLEJfNwZOGQEx4t/aS/GgxRAR+ra9pMHPMtTHU4sqqA=="], + + "@deepseek-ai/dsh-agent": ["@deepseek-ai/dsh-agent@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-agent/-/dsh-agent-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-scope": "0.1.1-rc.1", "@deepseek-ai/dsh-session": "0.1.2-alpha.2", "@deepseek-ai/dsh-session-projection": "0.1.1-rc.1", "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.1", "@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.2" } }, "sha512-K7B5XSQ7byB/IoNGj7n+lBgHCpVPJqEPvpGoHKc1dBS8fPo2yYp/ALFag4YOfrXVP3jQ9A8di20BbvIlp79SoA=="], + + "@deepseek-ai/dsh-attachment": ["@deepseek-ai/dsh-attachment@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-attachment/-/dsh-attachment-0.1.1-rc.1.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-R5xKO2z+91Ze25E+ghk3qDJf7ZA4t2sqMF1wusp2/6KrF0UzhCpGt/SWtdjRXSwz6DTjsAfZzpPC5D+2EmuHxA=="], + + "@deepseek-ai/dsh-brand": ["@deepseek-ai/dsh-brand@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-brand/-/dsh-brand-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-sWFbShCe8LNLuqD3gdQeneUGxFtZ/rywe6Tmi2eteBDubecHnf04XurtF38Epno42bnNTMYWuKwt1cihcbHL3A=="], + + "@deepseek-ai/dsh-client-locale": ["@deepseek-ai/dsh-client-locale@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-client-locale/-/dsh-client-locale-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-YDALYBD+iYU3GAAdL0ce2edcKURGe8jTk452ZSmDTGaUqd4TUBsB304yt2HjSzAyzoL3R7t2f4aKrVtAWbY1Rg=="], + + "@deepseek-ai/dsh-client-ui-conversation": ["@deepseek-ai/dsh-client-ui-conversation@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-conversation/-/dsh-client-ui-conversation-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.2", "@lexical/history": "0.49.0", "@lexical/plain-text": "0.49.0", "@lexical/text": "0.49.0", "@lexical/utils": "0.49.0", "clsx": "2.1.1", "lexical": "0.49.0" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-iCNkro90sUYopvtAWe3pE635ZpKfdUQdvejTboEVmmwBvU6n+2yKQDOcDMntZO7OqDOD4LfoJlMnnG3OGrXxtw=="], + + "@deepseek-ai/dsh-client-ui-primitives": ["@deepseek-ai/dsh-client-ui-primitives@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-primitives/-/dsh-client-ui-primitives-0.1.2-alpha.2.tgz", { "dependencies": { "@shikijs/langs": "4.4.3", "@types/mdast": "4.0.4", "anser": "2.3.5", "clsx": "2.1.1", "katex": "0.16.47", "mdast-util-from-markdown": "2.0.3", "mdast-util-gfm": "3.1.0", "mdast-util-math": "3.0.0", "micromark-core-commonmark": "2.0.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-math": "3.1.0", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-classify-character": "2.0.1", "micromark-util-sanitize-uri": "2.0.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2", "react": "18.2.0", "react-dom": "18.2.0", "shiki": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-OKOfYgM4KxACMJEprT0ThIDTJetWpYp5qNqULwJmc/4CY+sTQmFQ2tJjOXLwNpkCrryVJF2YZet/6j1JNp2h5w=="], + + "@deepseek-ai/dsh-client-ui-settings": ["@deepseek-ai/dsh-client-ui-settings@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-settings/-/dsh-client-ui-settings-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-06gPdKaUVItJt1liQVYM/I2W0TxNuK7qN682Ja43R4vMahg++mP35ALA5MtZPXQNLnHKe+AllYMzmgrjBf8rnA=="], + + "@deepseek-ai/dsh-client-ui-slots": ["@deepseek-ai/dsh-client-ui-slots@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-client-ui-slots/-/dsh-client-ui-slots-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-6ihXjJcgQLExj28SkKuPK0Rr7xfOvHhOiAIeO+uD5yywxtz7mSAW4S9JuxAOrpk2I2gNNU0DnFU4MmB229tjww=="], + + "@deepseek-ai/dsh-code-runtime": ["@deepseek-ai/dsh-code-runtime@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-code-runtime/-/dsh-code-runtime-0.1.1-rc.1.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-uha3FJobyShXIMfSuyGCHatbcfY3PA4mATS+hu3JoJy/+9w2Hbish89+eTZBysA4XEG+FRzVVdz9TbAHP3UR9Q=="], + + "@deepseek-ai/dsh-host-webserver": ["@deepseek-ai/dsh-host-webserver@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-host-webserver/-/dsh-host-webserver-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.2", "compression": "1.8.1", "negotiator": "1.1.0" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-cvsfM/cm5hZk/RqdIsardfqBIVpemdmUrP4M6UgdqhJy2nG5VnokLBg7k0bc8Yi11q0vIETfQK4xiDOSOMnu7Q=="], + + "@deepseek-ai/dsh-invariants": ["@deepseek-ai/dsh-invariants@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-invariants/-/dsh-invariants-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-1ewUeCzUHbaqhtW5rG1/eujIXXzy2VhwvMa16RpcTuJp5qcU1NtAf/+COkmvI7qtkyNY61vWg1Ez5qL9hKIUpQ=="], + + "@deepseek-ai/dsh-llm": ["@deepseek-ai/dsh-llm@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-llm/-/dsh-llm-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-timeout": "0.1.2-alpha.2", "@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-crypto": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-values": "0.1.2-alpha.2", "@deepseek-ai/schemastery": "3.18.2", "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-ip6yMxwHugxQm4VCbwX/FDnlTeeBM9VBkIn0+74ityQy7Z3yKREJ1Ov8Z04l4G3duRzeGRsQ4ztOFZ01oNfKIw=="], + + "@deepseek-ai/dsh-scope": ["@deepseek-ai/dsh-scope@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-scope/-/dsh-scope-0.1.1-rc.1.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-1Zt+tWDSCoSTAzbaJdSiWHe3pJTpyIqnsCEP3SOyxLIKclsj378rhsrkw+5O6oCRxjDCf0u8BJ+Q5liXREedBg=="], + + "@deepseek-ai/dsh-session": ["@deepseek-ai/dsh-session@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-session/-/dsh-session-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-values": "0.1.2-alpha.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-scope": "0.1.1-rc.1" } }, "sha512-RfikXscYTDXDr7CD7C/8oGJZaH8Egclj7pmXRtd90QcB5L8RIQ7069xrHZjds8OjNrFo69qQwNK3gYLUVZy9PA=="], + + "@deepseek-ai/dsh-session-projection": ["@deepseek-ai/dsh-session-projection@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-session-projection/-/dsh-session-projection-0.1.1-rc.1.tgz", { "dependencies": { "zod": "4.4.3" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-session": "0.1.2-alpha.2" } }, "sha512-RXDAUdfi3CqTQGQhMyIDZ5u4nURmN0ChkWwoX5Ph5rITa1zelnQsskYc93s+FTtB9Ituac3Uynk0rKGw/bS4fw=="], + + "@deepseek-ai/dsh-settings": ["@deepseek-ai/dsh-settings@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-settings/-/dsh-settings-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/dsh-util-values": "0.1.2-alpha.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-session": "0.1.2-alpha.2", "@deepseek-ai/schemastery": "3.18.2" } }, "sha512-rWPH/LfDU9SCFaonqfxCMjnD4gkO31DlQVHL9LIy7wH/FVqnBJ6v13YN70Fpof2KKPyL2YhcwOOPQbusxlzSDw=="], + + "@deepseek-ai/dsh-subagent": ["@deepseek-ai/dsh-subagent@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-subagent/-/dsh-subagent-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-values": "0.1.2-alpha.2", "zod": "4.4.3" }, "optionalDependencies": { "@deepseek-ai/dsh-session-projection": "0.1.1-rc.1", "@deepseek-ai/dsh-user-approval": "0.1.1-rc.1" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-agent": "0.1.2-alpha.2", "@deepseek-ai/dsh-attachment": "0.1.1-rc.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-scope": "0.1.1-rc.1", "@deepseek-ai/dsh-session": "0.1.2-alpha.2", "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.1", "@deepseek-ai/dsh-tools": "0.1.2-alpha.2", "@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-time": "0.1.2-alpha.2" } }, "sha512-WN268rZqdo/lSO4ZQ6tIPaBS0Bs0QCC7lNwYTaZTaKOml7k+EYZSSDeRF1tJlfM8FjwDtYdB9pHUqLHyNKcaVg=="], + + "@deepseek-ai/dsh-system-prompt": ["@deepseek-ai/dsh-system-prompt@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-system-prompt/-/dsh-system-prompt-0.1.1-rc.1.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.1" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-scope": "0.1.1-rc.1" } }, "sha512-YILPMh2sfwhcK1ijr9ll7TKVQPwhhfrxIFl/Kdi+LIeLiTlOOnt6krpGnlXfzRPu7SzpNnwcetKljKOrHtY2YQ=="], + + "@deepseek-ai/dsh-timeout": ["@deepseek-ai/dsh-timeout@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-timeout/-/dsh-timeout-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-8q5cd55aMoOvrPaqSws/3xiyzHhs1bfjdtAs4YHWimQgMd+yMrDnlu8i+zFOkWoSc0A2wPkXcCYR8xogl4gerA=="], + + "@deepseek-ai/dsh-tools": ["@deepseek-ai/dsh-tools@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-tools/-/dsh-tools-0.1.2-alpha.2.tgz", { "dependencies": { "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-util-values": "0.1.2-alpha.2", "@deepseek-ai/schemastery": "3.18.2" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-agent": "0.1.2-alpha.2", "@deepseek-ai/dsh-code-runtime": "0.1.1-rc.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-scope": "0.1.1-rc.1", "@deepseek-ai/dsh-session": "0.1.2-alpha.2", "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.1", "@deepseek-ai/dsh-user-approval": "0.1.1-rc.1" } }, "sha512-trk0fkmCDp64pqdcr8u7rCcRrwNi+93FKuznTnCD+YsPGFygcSG/6n+Wsh4+9A6oI1fM4/Ecq6Baa9vq1sNhJg=="], + + "@deepseek-ai/dsh-typert-protocol": ["@deepseek-ai/dsh-typert-protocol@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-typert-protocol/-/dsh-typert-protocol-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-U3j/usWHRllaJMdTr4GuVkkUciHWyq01mvMtWeGT+RY2hb+ysTxeuLEiV0Lh5EyVScgFdDLbe/GtTZ2OkGsGwQ=="], + + "@deepseek-ai/dsh-user-approval": ["@deepseek-ai/dsh-user-approval@0.1.1-rc.1", "https://registry.npmmirror.com/@deepseek-ai/dsh-user-approval/-/dsh-user-approval-0.1.1-rc.1.tgz", { "dependencies": { "@deepseek-ai/schemastery": "3.18.1" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-agent": "0.1.2-alpha.2", "@deepseek-ai/dsh-brand": "0.1.2-alpha.2", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "@deepseek-ai/dsh-llm": "0.1.2-alpha.2", "@deepseek-ai/dsh-scope": "0.1.1-rc.1", "@deepseek-ai/dsh-session": "0.1.2-alpha.2", "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.1" } }, "sha512-r2TkC0JJGQ8EOyTH8DXafbcj080NdNbVzoNERA1pRTrH7+ZE0PHU6MZU7xGy7cwAKmAUGnm1DjF8UHOQN6ns2Q=="], + + "@deepseek-ai/dsh-util-crypto": ["@deepseek-ai/dsh-util-crypto@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-util-crypto/-/dsh-util-crypto-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-03WVzlgLErJRRGMqaMdx5ufEoN3Scpdsnd0f8vXMhcbeEzKZ61lbI8z/m6ZttM/LhpzPALuFtDc5u+N5LFP8Fg=="], + + "@deepseek-ai/dsh-util-time": ["@deepseek-ai/dsh-util-time@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-util-time/-/dsh-util-time-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-BhVWSkq/GisaTDRIjg8MeUG9CaltZ2KlmcNO23gWo3ns+SEF7W8DpmHpcVXWLRILvcq4J+w9gfcw2leSnPuGCg=="], + + "@deepseek-ai/dsh-util-values": ["@deepseek-ai/dsh-util-values@0.1.2-alpha.2", "https://registry.npmmirror.com/@deepseek-ai/dsh-util-values/-/dsh-util-values-0.1.2-alpha.2.tgz", { "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2" } }, "sha512-ZyZHTqGQ/8S5ZYflTkuiZGPxhkuHa9Uy5G+teEmnmnkhiN/UVPGQvI//CXRNmxpTKjfx+qcVVmr5x6bohplsQg=="], + + "@deepseek-ai/schemastery": ["@deepseek-ai/schemastery@3.18.2", "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.2.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.3", "@standard-schema/spec": "1.1.0" } }, "sha512-njDtZsznjYxok7KLLlHOPyuv2efdWVbSflAHgztSfbMsg+CVraEoRe2DjOCgClYv3ZCSm7WXoaUkbB/+RY7tWQ=="], + + "@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@exodus/bytes": ["@exodus/bytes@1.15.1", "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", {}, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], + + "@huanlin/dsh-plugin-better-locale": ["@huanlin/dsh-plugin-better-locale@0.1.0", "https://registry.npmmirror.com/@huanlin/dsh-plugin-better-locale/-/dsh-plugin-better-locale-0.1.0.tgz", { "optionalDependencies": { "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2", "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2", "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2", "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2", "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2", "react": "18.2.0", "react-dom": "18.2.0" }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1" } }, "sha512-fGXDgVq1R3gVTi2M9TMn2x5ROqiaYkhEsFJqYF5xiVUFfs9KjwIpOF0ZbhUyK6vFBOeYP4uetc/ZVknQb38WrQ=="], + + "@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.4", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.4.tgz", { "dependencies": { "@antfu/install-pkg": "1.1.0", "@iconify/types": "2.0.0", "import-meta-resolve": "4.2.0" } }, "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@lexical/clipboard": ["@lexical/clipboard@0.49.0", "https://registry.npmmirror.com/@lexical/clipboard/-/clipboard-0.49.0.tgz", { "dependencies": { "@lexical/extension": "0.49.0", "@lexical/html": "0.49.0", "@lexical/internal": "0.49.0", "@lexical/list": "0.49.0", "@lexical/selection": "0.49.0", "@lexical/utils": "0.49.0", "@types/trusted-types": "2.0.7", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-AVKj21xH1qU7JAFA/v0hCoafa+Yti1I7cHG+JQIgc/EqCtP8ePeJyIfTPNN/tXskoCdq++HewQoiSXRwwlocVg=="], + + "@lexical/dragon": ["@lexical/dragon@0.49.0", "https://registry.npmmirror.com/@lexical/dragon/-/dragon-0.49.0.tgz", { "dependencies": { "@lexical/extension": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-62/4DP5qyX/l4Yf5qRyyQrs9BV725eRU3OmLUW6g7T5xrcHXxAo7tia/NvqjqvXdpvQzyHjWgsy7dMitGITUsw=="], + + "@lexical/extension": ["@lexical/extension@0.49.0", "https://registry.npmmirror.com/@lexical/extension/-/extension-0.49.0.tgz", { "dependencies": { "@lexical/internal": "0.49.0", "@lexical/utils": "0.49.0", "@preact/signals-core": "1.14.4", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-Wv0VsuqxorbxHCK4ms1PwAu6cXIGNLtflq66auF+zwxOtBprkSFV8VzzzdKLOYD2admP0VK6EqVCeGXFK1GggA=="], + + "@lexical/history": ["@lexical/history@0.49.0", "https://registry.npmmirror.com/@lexical/history/-/history-0.49.0.tgz", { "dependencies": { "@lexical/extension": "0.49.0", "@lexical/utils": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-uQdtEd34gIJklXNSdHS2Wko1zxx1xUMVXbiodcLO6a3GeFTE5bKnx6af1zGX78KpYhUQYnHWorKuUvtdZlJPaA=="], + + "@lexical/html": ["@lexical/html@0.49.0", "https://registry.npmmirror.com/@lexical/html/-/html-0.49.0.tgz", { "dependencies": { "@lexical/extension": "0.49.0", "@lexical/internal": "0.49.0", "@lexical/selection": "0.49.0", "@lexical/utils": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-NQqAydKzRjQl7Jx+bTTyC2iuz5uhuDaQX0SSPrdfgaFDCPjqQEejcBkCt1QXnu1CkbVHutZKVGVzljx40Y6y+Q=="], + + "@lexical/internal": ["@lexical/internal@0.49.0", "https://registry.npmmirror.com/@lexical/internal/-/internal-0.49.0.tgz", { "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-s+XjPC7Qb39A/Xx9ahcz1s69CPix4ultaqyW+MDG0AXYW2quDr7m0USqReKIAiuoLIWtGN5HW8BwV2Y6t4qr6Q=="], + + "@lexical/list": ["@lexical/list@0.49.0", "https://registry.npmmirror.com/@lexical/list/-/list-0.49.0.tgz", { "dependencies": { "@lexical/extension": "0.49.0", "@lexical/html": "0.49.0", "@lexical/internal": "0.49.0", "@lexical/utils": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-zs6wYkxakDRcJO0KmwrPPHgeLLIxzjD+P2CRu+scJCHRuA5e2iSw2iFjH5n/LlWBrlnPMSjeQse4QqsRy9nnqA=="], + + "@lexical/plain-text": ["@lexical/plain-text@0.49.0", "https://registry.npmmirror.com/@lexical/plain-text/-/plain-text-0.49.0.tgz", { "dependencies": { "@lexical/clipboard": "0.49.0", "@lexical/dragon": "0.49.0", "@lexical/extension": "0.49.0", "@lexical/selection": "0.49.0", "@lexical/utils": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-l7IuUj9n9CtFfz4Fz6zU6mmO+VkcnvyyebABx+lHevvTa7B6YI5PPVgABFfzdyPanyW/FGs7qpKBf59inAqbkg=="], + + "@lexical/selection": ["@lexical/selection@0.49.0", "https://registry.npmmirror.com/@lexical/selection/-/selection-0.49.0.tgz", { "dependencies": { "@lexical/internal": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-08Vd1+VoC6YnztWOFWVsqF/Hxw5EP8qeL1c7t3+rVCV8revLzXxdQw+vbPX3tgM4fmjOBDUXk6Ws1+rTtsqjcQ=="], + + "@lexical/text": ["@lexical/text@0.49.0", "https://registry.npmmirror.com/@lexical/text/-/text-0.49.0.tgz", { "dependencies": { "@lexical/internal": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-mowedvbvx0HDaW+ymVdYmWVhueqgauhhqIWaCtbJj33tPk8pOu1BB0pZuJQbOB+1bDnFiZhkJV8W/CvR2M9lEA=="], + + "@lexical/utils": ["@lexical/utils@0.49.0", "https://registry.npmmirror.com/@lexical/utils/-/utils-0.49.0.tgz", { "dependencies": { "@lexical/internal": "0.49.0", "@lexical/selection": "0.49.0", "lexical": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-Jaa6DERBqxiFOFa49VPRV1WOb7mzRbMZ5U+v+RFagjzTmBhfxDmEkbQQ9nYTU3n3DPfdT8Hkyffextmw04etXg=="], + + "@lezer/common": ["@lezer/common@1.5.2", "https://registry.npmmirror.com/@lezer/common/-/common-1.5.2.tgz", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + + "@lezer/cpp": ["@lezer/cpp@1.1.6", "https://registry.npmmirror.com/@lezer/cpp/-/cpp-1.1.6.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA=="], + + "@lezer/css": ["@lezer/css@1.3.6", "https://registry.npmmirror.com/@lezer/css/-/css-1.3.6.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g=="], + + "@lezer/go": ["@lezer/go@1.0.1", "https://registry.npmmirror.com/@lezer/go/-/go-1.0.1.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz", { "dependencies": { "@lezer/common": "1.5.2" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/html": ["@lezer/html@1.3.13", "https://registry.npmmirror.com/@lezer/html/-/html-1.3.13.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="], + + "@lezer/java": ["@lezer/java@1.1.3", "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.4.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/json": ["@lezer/json@1.0.3", "https://registry.npmmirror.com/@lezer/json/-/json-1.0.3.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="], + + "@lezer/lr": ["@lezer/lr@1.4.10", "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.10.tgz", { "dependencies": { "@lezer/common": "1.5.2" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="], + + "@lezer/markdown": ["@lezer/markdown@1.7.2", "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.7.2.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3" } }, "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ=="], + + "@lezer/php": ["@lezer/php@1.0.5", "https://registry.npmmirror.com/@lezer/php/-/php-1.0.5.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA=="], + + "@lezer/python": ["@lezer/python@1.1.19", "https://registry.npmmirror.com/@lezer/python/-/python-1.1.19.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ=="], + + "@lezer/rust": ["@lezer/rust@1.0.2", "https://registry.npmmirror.com/@lezer/rust/-/rust-1.0.2.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg=="], + + "@lezer/xml": ["@lezer/xml@1.0.6", "https://registry.npmmirror.com/@lezer/xml/-/xml-1.0.6.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="], + + "@lezer/yaml": ["@lezer/yaml@1.0.4", "https://registry.npmmirror.com/@lezer/yaml/-/yaml-1.0.4.tgz", { "dependencies": { "@lezer/common": "1.5.2", "@lezer/highlight": "1.2.3", "@lezer/lr": "1.4.10" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.3", "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", {}, "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA=="], + + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.2.1.tgz", { "dependencies": { "@chevrotain/types": "11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", { "dependencies": { "@tybys/wasm-util": "0.10.3" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="], + + "@oxc-project/types": ["@oxc-project/types@0.146.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.146.0.tgz", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], + + "@playwright/test": ["@playwright/test@1.62.1", "https://registry.npmmirror.com/@playwright/test/-/test-1.62.1.tgz", { "dependencies": { "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" } }, "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ=="], + + "@preact/signals-core": ["@preact/signals-core@1.14.4", "https://registry.npmmirror.com/@preact/signals-core/-/signals-core-1.14.4.tgz", {}, "sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA=="], + + "@quansync/fs": ["@quansync/fs@1.0.0", "https://registry.npmmirror.com/@quansync/fs/-/fs-1.0.0.tgz", { "dependencies": { "quansync": "1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "1.2.3" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@shikijs/core": ["@shikijs/core@4.4.3", "https://registry.npmmirror.com/@shikijs/core/-/core-4.4.3.tgz", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.5", "hast-util-to-html": "9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "10.0.2", "oniguruma-to-es": "4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="], + + "@shikijs/langs": ["@shikijs/langs@4.4.3", "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.4.3.tgz", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.4.3", "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.4.3.tgz", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="], + + "@shikijs/themes": ["@shikijs/themes@4.4.3", "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.4.3.tgz", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="], + + "@shikijs/types": ["@shikijs/types@4.4.3", "https://registry.npmmirror.com/@shikijs/types/-/types-4.4.3.tgz", { "dependencies": { "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "@types/chai": ["@types/chai@5.2.3", "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/d3": ["@types/d3@7.4.3", "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", { "dependencies": { "@types/d3-array": "3.2.2", "@types/d3-axis": "3.0.6", "@types/d3-brush": "3.0.6", "@types/d3-chord": "3.0.6", "@types/d3-color": "3.1.3", "@types/d3-contour": "3.0.6", "@types/d3-delaunay": "6.0.4", "@types/d3-dispatch": "3.0.7", "@types/d3-drag": "3.0.7", "@types/d3-dsv": "3.0.7", "@types/d3-ease": "3.0.2", "@types/d3-fetch": "3.0.7", "@types/d3-force": "3.0.10", "@types/d3-format": "3.0.4", "@types/d3-geo": "3.1.1", "@types/d3-hierarchy": "3.1.7", "@types/d3-interpolate": "3.0.4", "@types/d3-path": "3.1.1", "@types/d3-polygon": "3.0.2", "@types/d3-quadtree": "3.0.6", "@types/d3-random": "3.0.4", "@types/d3-scale": "4.0.9", "@types/d3-scale-chromatic": "3.1.0", "@types/d3-selection": "3.0.11", "@types/d3-shape": "3.1.8", "@types/d3-time": "3.0.4", "@types/d3-time-format": "4.0.3", "@types/d3-timer": "3.0.2", "@types/d3-transition": "3.0.9", "@types/d3-zoom": "3.0.8" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "3.0.11" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "3.0.11" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", { "dependencies": { "@types/d3-array": "3.2.2", "@types/geojson": "7946.0.16" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", { "dependencies": { "@types/d3-selection": "3.0.11" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", { "dependencies": { "@types/d3-dsv": "3.0.7" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.1", "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.1.tgz", { "dependencies": { "@types/geojson": "7946.0.16" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "3.1.3" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.4", "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.4.tgz", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", { "dependencies": { "@types/d3-time": "3.0.4" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", { "dependencies": { "@types/d3-path": "3.1.1" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", { "dependencies": { "@types/d3-selection": "3.0.11" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", { "dependencies": { "@types/d3-interpolate": "3.0.4", "@types/d3-selection": "3.0.11" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "2.1.0" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/hast": ["@types/hast@3.0.5", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.5.tgz", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + + "@types/katex": ["@types/katex@0.16.8", "https://registry.npmmirror.com/@types/katex/-/katex-0.16.8.tgz", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/mdast": ["@types/mdast@4.0.4", "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@24.13.3", "https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz", { "dependencies": { "undici-types": "7.18.2" } }, "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.31", "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", { "dependencies": { "@types/prop-types": "15.7.15", "csstype": "3.2.3" } }, "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw=="], + + "@types/react-dom": ["@types/react-dom@18.3.1", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.1.tgz", { "dependencies": { "@types/react": "18.3.31" } }, "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/ws": ["@types/ws@8.18.1", "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", { "dependencies": { "@types/node": "24.13.3" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "3.0.0", "d3-transition": "3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + + "@vitest/expect": ["@vitest/expect@4.1.11", "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.11.tgz", { "dependencies": { "@standard-schema/spec": "1.1.0", "@types/chai": "5.2.3", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "6.2.2", "tinyrainbow": "3.1.1" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.11", "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.11.tgz", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "3.0.3", "magic-string": "0.30.21" }, "optionalDependencies": { "vite": "8.2.2" } }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", { "dependencies": { "tinyrainbow": "3.1.1" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], + + "@vitest/runner": ["@vitest/runner@4.1.11", "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.11.tgz", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.11.tgz", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "0.30.21", "pathe": "2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], + + "@vitest/spy": ["@vitest/spy@4.1.11", "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.11.tgz", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], + + "@vitest/utils": ["@vitest/utils@4.1.11", "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.11.tgz", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "2.0.0", "tinyrainbow": "3.1.1" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], + + "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "https://registry.npmmirror.com/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], + + "@xterm/xterm": ["@xterm/xterm@5.5.0", "https://registry.npmmirror.com/@xterm/xterm/-/xterm-5.5.0.tgz", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="], + + "@yuku-codegen/binding-android-arm64": ["@yuku-codegen/binding-android-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-android-arm64/-/binding-android-arm64-0.8.7.tgz", { "os": "android", "cpu": "arm64" }, "sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw=="], + + "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-darwin-arm64/-/binding-darwin-arm64-0.8.7.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw=="], + + "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-darwin-x64/-/binding-darwin-x64-0.8.7.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw=="], + + "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-freebsd-x64/-/binding-freebsd-x64-0.8.7.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw=="], + + "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.8.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg=="], + + "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-arm-musl/-/binding-linux-arm-musl-0.8.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q=="], + + "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.8.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A=="], + + "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.8.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA=="], + + "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.8.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw=="], + + "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-linux-x64-musl/-/binding-linux-x64-musl-0.8.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA=="], + + "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-win32-arm64/-/binding-win32-arm64-0.8.7.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA=="], + + "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.8.7", "https://registry.npmmirror.com/@yuku-codegen/binding-win32-x64/-/binding-win32-x64-0.8.7.tgz", { "os": "win32", "cpu": "x64" }, "sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA=="], + + "@yuku-parser/binding-android-arm64": ["@yuku-parser/binding-android-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-android-arm64/-/binding-android-arm64-0.8.7.tgz", { "os": "android", "cpu": "arm64" }, "sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ=="], + + "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.8.7.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ=="], + + "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-darwin-x64/-/binding-darwin-x64-0.8.7.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA=="], + + "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.8.7.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA=="], + + "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-arm-gnu/-/binding-linux-arm-gnu-0.8.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg=="], + + "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-arm-musl/-/binding-linux-arm-musl-0.8.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ=="], + + "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.8.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA=="], + + "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.8.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg=="], + + "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.8.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg=="], + + "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.8.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ=="], + + "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-win32-arm64/-/binding-win32-arm64-0.8.7.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw=="], + + "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.8.7", "https://registry.npmmirror.com/@yuku-parser/binding-win32-x64/-/binding-win32-x64-0.8.7.tgz", { "os": "win32", "cpu": "x64" }, "sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w=="], + + "@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.7", "https://registry.npmmirror.com/@yuku-toolchain/types/-/types-0.8.7.tgz", {}, "sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA=="], + + "anser": ["anser@2.3.5", "https://registry.npmmirror.com/anser/-/anser-2.3.5.tgz", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="], + + "ansis": ["ansis@4.3.1", "https://registry.npmmirror.com/ansis/-/ansis-4.3.1.tgz", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="], + + "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "assertion-error": ["assertion-error@2.0.1", "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "bidi-js": ["bidi-js@1.0.3", "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", { "dependencies": { "require-from-string": "2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@7.0.0", "https://registry.npmmirror.com/cac/-/cac-7.0.0.tgz", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], + + "ccount": ["ccount@2.0.1", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chai": ["chai@6.2.2", "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "character-entities": ["character-entities@2.0.2", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "compressible": ["compressible@2.0.18", "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", { "dependencies": { "mime-db": "1.54.0" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.8.1", "https://registry.npmmirror.com/compression/-/compression-1.8.1.tgz", { "dependencies": { "bytes": "3.1.2", "compressible": "2.0.18", "debug": "2.6.9", "negotiator": "0.6.4", "on-headers": "1.1.0", "safe-buffer": "5.2.1", "vary": "1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], + + "content-type": ["content-type@2.1.0", "https://registry.npmmirror.com/content-type/-/content-type-2.1.0.tgz", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cordis": ["cordis@4.0.0-rc.8", "https://registry.npmmirror.com/cordis/-/cordis-4.0.0-rc.8.tgz", { "dependencies": { "@standard-schema/spec": "1.1.0", "cosmokit": "1.8.1" }, "optionalDependencies": { "@cordisjs/plugin-loader": "1.0.0-rc.5" }, "bin": "bin.js" }, "sha512-vXaYK6XZJlIFTnODp4Rd973Qnd/gm3cwFzWsMTaeu6cQQheA7N+aA0GqHRNl0cFIrxMXWU8dst1ZXHlUQvfyRw=="], + + "cose-base": ["cose-base@1.0.3", "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", { "dependencies": { "layout-base": "1.0.2" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + + "cosmokit": ["cosmokit@1.8.1", "https://registry.npmmirror.com/cosmokit/-/cosmokit-1.8.1.tgz", {}, "sha512-PDBv4l90xZKrUsZ0vtoycgZpO/j4iFsqJXrAxsyBDsnQRI7ZMJXIjgDJsKNjd5L8jnVnnlrDCdhkFbTncgCVjQ=="], + + "crelt": ["crelt@1.0.7", "https://registry.npmmirror.com/crelt/-/crelt-1.0.7.tgz", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="], + + "css-tree": ["css-tree@3.2.1", "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "cytoscape": ["cytoscape@3.34.1", "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.34.1.tgz", {}, "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", { "dependencies": { "cose-base": "1.0.3" }, "peerDependencies": { "cytoscape": "3.34.1" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", { "dependencies": { "cose-base": "2.2.0" }, "peerDependencies": { "cytoscape": "3.34.1" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", { "dependencies": { "d3-array": "3.2.4", "d3-axis": "3.0.0", "d3-brush": "3.0.0", "d3-chord": "3.0.1", "d3-color": "3.1.0", "d3-contour": "4.0.2", "d3-delaunay": "6.0.4", "d3-dispatch": "3.0.1", "d3-drag": "3.0.0", "d3-dsv": "3.0.1", "d3-ease": "3.0.1", "d3-fetch": "3.0.1", "d3-force": "3.0.0", "d3-format": "3.1.2", "d3-geo": "3.1.1", "d3-hierarchy": "3.1.2", "d3-interpolate": "3.0.1", "d3-path": "3.1.0", "d3-polygon": "3.0.1", "d3-quadtree": "3.0.1", "d3-random": "3.0.1", "d3-scale": "4.0.2", "d3-scale-chromatic": "3.1.0", "d3-selection": "3.0.0", "d3-shape": "3.2.0", "d3-time": "3.1.0", "d3-time-format": "4.1.0", "d3-timer": "3.0.1", "d3-transition": "3.0.1", "d3-zoom": "3.0.0" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", { "dependencies": { "internmap": "2.0.3" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", { "dependencies": { "d3-dispatch": "3.0.1", "d3-drag": "3.0.0", "d3-interpolate": "3.0.1", "d3-selection": "3.0.0", "d3-transition": "3.0.1" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", { "dependencies": { "d3-path": "3.1.0" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", { "dependencies": { "d3-array": "3.2.4" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", { "dependencies": { "delaunator": "5.1.0" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", { "dependencies": { "d3-dispatch": "3.0.1", "d3-selection": "3.0.0" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", { "dependencies": { "commander": "7.0.0", "iconv-lite": "0.6.0", "rw": "1.3.3" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", { "dependencies": { "d3-dsv": "3.0.1" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", { "dependencies": { "d3-dispatch": "3.0.1", "d3-quadtree": "3.0.1", "d3-timer": "3.0.1" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", { "dependencies": { "d3-array": "3.2.4" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "3.1.0" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", { "dependencies": { "d3-array": "2.12.1", "d3-shape": "1.3.7" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", { "dependencies": { "d3-array": "3.2.4", "d3-format": "3.1.2", "d3-interpolate": "3.0.1", "d3-time": "3.1.0", "d3-time-format": "4.1.0" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", { "dependencies": { "d3-color": "3.1.0", "d3-interpolate": "3.0.1" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", { "dependencies": { "d3-path": "3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", { "dependencies": { "d3-array": "3.2.4" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", { "dependencies": { "d3-time": "3.1.0" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", { "dependencies": { "d3-color": "3.1.0", "d3-dispatch": "3.0.1", "d3-ease": "3.0.1", "d3-interpolate": "3.0.1", "d3-timer": "3.0.1" }, "peerDependencies": { "d3-selection": "3.0.0" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", { "dependencies": { "d3-dispatch": "3.0.1", "d3-drag": "3.0.0", "d3-interpolate": "3.0.1", "d3-selection": "3.0.0", "d3-transition": "3.0.1" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", { "dependencies": { "d3": "7.9.0", "lodash-es": "4.18.1" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "data-urls": ["data-urls@7.0.0", "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", { "dependencies": { "whatwg-mimetype": "5.0.0", "whatwg-url": "16.0.1" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "dayjs": ["dayjs@1.11.23", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], + + "debug": ["debug@2.6.9", "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "2.0.2" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "defu": ["defu@6.1.7", "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "delaunator": ["delaunator@5.1.0", "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", { "dependencies": { "robust-predicates": "3.0.3" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + + "dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devlop": ["devlop@1.1.0", "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "2.0.3" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "dompurify": ["dompurify@3.4.14", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.14.tgz", { "optionalDependencies": { "@types/trusted-types": "2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="], + + "dts-resolver": ["dts-resolver@3.0.0", "https://registry.npmmirror.com/dts-resolver/-/dts-resolver-3.0.0.tgz", {}, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="], + + "empathic": ["empathic@2.0.1", "https://registry.npmmirror.com/empathic/-/empathic-2.0.1.tgz", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="], + + "entities": ["entities@8.0.0", "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + + "es-module-lexer": ["es-module-lexer@2.3.2", "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], + + "es-toolkit": ["es-toolkit@1.51.0", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.51.0.tgz", {}, "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw=="], + + "escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "1.0.9" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "fastdom": ["fastdom@1.0.12", "https://registry.npmmirror.com/fastdom/-/fastdom-1.0.12.tgz", { "dependencies": { "strictdom": "1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="], + + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "optionalDependencies": { "picomatch": "4.0.5" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.2", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", { "dependencies": { "resolve-pkg-maps": "1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], + + "hachure-fill": ["hachure-fill@0.5.2", "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", { "dependencies": { "@types/hast": "3.0.5", "@types/unist": "3.0.3", "ccount": "2.0.1", "comma-separated-tokens": "2.0.3", "hast-util-whitespace": "3.0.0", "html-void-elements": "3.0.0", "mdast-util-to-hast": "13.2.1", "property-information": "7.2.0", "space-separated-tokens": "2.0.2", "stringify-entities": "4.0.4", "zwitch": "2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", { "dependencies": { "@types/hast": "3.0.5" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hookable": ["hookable@6.1.1", "https://registry.npmmirror.com/hookable/-/hookable-6.1.1.tgz", {}, "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", { "dependencies": { "@exodus/bytes": "1.15.1" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], + + "html-void-elements": ["html-void-elements@3.0.0", "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "iconv-lite": ["iconv-lite@0.6.0", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.0.tgz", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-43ZpGYZ9QtuutX5l6WC1DSO8ane9N+Ct5qPLF2OV7vM9abM69gnAbVkh66ibaZd3aOGkoP1ZmringlKhLBkw2Q=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "import-without-cache": ["import-without-cache@0.4.0", "https://registry.npmmirror.com/import-without-cache/-/import-without-cache-0.4.0.tgz", {}, "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ=="], + + "internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "js-tokens": ["js-tokens@3.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-3.0.0.tgz", {}, "sha512-poXEQHPMmTrYZuJgNRll2sbc3kJsSU1m/g1Q93IE6txNj3p6xOOOmdj1G/zCVGawYSPzTkSoWGg1otqbeqKJeg=="], + + "js-yaml": ["js-yaml@4.3.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz", { "dependencies": { "argparse": "2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], + + "jsdom": ["jsdom@29.1.1", "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", { "dependencies": { "@asamuzakjp/css-color": "5.1.11", "@asamuzakjp/dom-selector": "7.1.1", "@bramus/specificity": "2.4.2", "@csstools/css-syntax-patches-for-csstree": "1.1.8", "@exodus/bytes": "1.15.1", "css-tree": "3.2.1", "data-urls": "7.0.0", "decimal.js": "10.6.0", "html-encoding-sniffer": "6.0.0", "is-potential-custom-element-name": "1.0.1", "lru-cache": "11.5.2", "parse5": "8.0.1", "saxes": "6.0.0", "symbol-tree": "3.2.4", "tough-cookie": "6.0.2", "undici": "7.29.0", "w3c-xmlserializer": "5.0.0", "webidl-conversions": "8.0.1", "whatwg-mimetype": "5.0.0", "whatwg-url": "16.0.1", "xml-name-validator": "5.0.0" } }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="], + + "katex": ["katex@0.16.47", "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", { "dependencies": { "commander": "8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + + "layout-base": ["layout-base@1.0.2", "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + + "lexical": ["lexical@0.49.0", "https://registry.npmmirror.com/lexical/-/lexical-0.49.0.tgz", { "dependencies": { "@lexical/internal": "0.49.0" }, "optionalDependencies": { "typescript": "5.6.2" } }, "sha512-9V1ZIzGpJEd8rIN+nN7veL4fW4fFWbS66Un4JNqSZB4D5t9euzN9+3+jEXy83FjNjNy0MiiUI3+DQaGCLYko0w=="], + + "lightningcss": ["lightningcss@1.33.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + + "longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "3.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@11.5.2", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.2.tgz", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "markdown-table": ["markdown-table@3.0.4", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "marked": ["marked@16.3.0", "https://registry.npmmirror.com/marked/-/marked-16.3.0.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", { "dependencies": { "@types/mdast": "4.0.4", "escape-string-regexp": "5.0.0", "unist-util-is": "6.0.1", "unist-util-visit-parents": "6.0.2" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", { "dependencies": { "@types/mdast": "4.0.4", "@types/unist": "3.0.3", "decode-named-character-reference": "1.3.0", "devlop": "1.1.0", "mdast-util-to-string": "4.0.0", "micromark": "4.0.0", "micromark-util-decode-numeric-character-reference": "2.0.0", "micromark-util-decode-string": "2.0.1", "micromark-util-normalize-identifier": "2.0.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2", "unist-util-stringify-position": "4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", { "dependencies": { "mdast-util-from-markdown": "2.0.3", "mdast-util-gfm-autolink-literal": "2.0.1", "mdast-util-gfm-footnote": "2.1.0", "mdast-util-gfm-strikethrough": "2.0.0", "mdast-util-gfm-table": "2.0.0", "mdast-util-gfm-task-list-item": "2.0.0", "mdast-util-to-markdown": "2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", { "dependencies": { "@types/mdast": "4.0.4", "ccount": "2.0.1", "devlop": "1.1.0", "mdast-util-find-and-replace": "3.0.2", "micromark-util-character": "2.1.1" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "https://registry.npmmirror.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "devlop": "1.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.0.0", "micromark-util-normalize-identifier": "2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "devlop": "1.1.0", "markdown-table": "3.0.4", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "devlop": "1.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "https://registry.npmmirror.com/mdast-util-math/-/mdast-util-math-3.0.0.tgz", { "dependencies": { "@types/hast": "3.0.5", "@types/mdast": "4.0.4", "devlop": "1.1.0", "longest-streak": "3.1.0", "mdast-util-from-markdown": "2.0.3", "mdast-util-to-markdown": "2.1.0", "unist-util-remove-position": "5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "unist-util-is": "6.0.1" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", { "dependencies": { "@types/hast": "3.0.5", "@types/mdast": "4.0.4", "@ungap/structured-clone": "1.3.3", "devlop": "1.1.0", "micromark-util-sanitize-uri": "2.0.1", "trim-lines": "3.0.1", "unist-util-position": "5.0.0", "unist-util-visit": "5.1.0", "vfile": "6.0.3" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.0.0", "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.0.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "@types/unist": "3.0.3", "longest-streak": "3.1.0", "mdast-util-phrasing": "4.1.0", "mdast-util-to-string": "4.0.0", "micromark-util-decode-string": "2.0.1", "unist-util-visit": "5.1.0", "zwitch": "2.0.4" } }, "sha512-Ov3aWCYpb31/SkHNRlREvSZsF9ETBW/rXw4PooawZO8qy2MviDq4TI6kxX6zFmGa/zUX36STKIC/IpASQk596w=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", { "dependencies": { "@types/mdast": "4.0.4" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "mdn-data": ["mdn-data@2.27.1", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "mermaid": ["mermaid@11.17.0", "https://registry.npmmirror.com/mermaid/-/mermaid-11.17.0.tgz", { "dependencies": { "@braintree/sanitize-url": "7.1.2", "@iconify/utils": "3.1.4", "@mermaid-js/parser": "1.2.1", "@types/d3": "7.4.3", "@upsetjs/venn.js": "2.0.0", "cytoscape": "3.34.1", "cytoscape-cose-bilkent": "4.1.0", "cytoscape-fcose": "2.2.0", "d3": "7.9.0", "d3-sankey": "0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "1.11.23", "dompurify": "3.4.14", "es-toolkit": "1.51.0", "fastdom": "1.0.12", "katex": "0.16.47", "khroma": "2.1.0", "marked": "16.3.0", "roughjs": "4.6.6", "stylis": "4.4.0", "ts-dedent": "2.3.0", "uuid": "14.0.2" } }, "sha512-Jo9N377Wb4MSnHFPTbLi2SxFpsQl4eVHoxnW5U1Md9EazvgMp3s+4ohDxr81YNTgbn5Kj7HJ3yslrSJ52kwpbA=="], + + "micromark": ["micromark@4.0.0", "https://registry.npmmirror.com/micromark/-/micromark-4.0.0.tgz", { "dependencies": { "@types/debug": "4.1.13", "debug": "4.4.3", "decode-named-character-reference": "1.3.0", "devlop": "1.1.0", "micromark-core-commonmark": "2.0.3", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-chunked": "2.0.0", "micromark-util-combine-extensions": "2.0.0", "micromark-util-decode-numeric-character-reference": "2.0.0", "micromark-util-encode": "2.0.0", "micromark-util-normalize-identifier": "2.0.0", "micromark-util-resolve-all": "2.0.0", "micromark-util-sanitize-uri": "2.0.1", "micromark-util-subtokenize": "2.0.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", { "dependencies": { "decode-named-character-reference": "1.3.0", "devlop": "1.1.0", "micromark-factory-destination": "2.0.0", "micromark-factory-label": "2.0.0", "micromark-factory-space": "2.0.1", "micromark-factory-title": "2.0.0", "micromark-factory-whitespace": "2.0.0", "micromark-util-character": "2.1.1", "micromark-util-chunked": "2.0.0", "micromark-util-classify-character": "2.0.1", "micromark-util-html-tag-name": "2.0.0", "micromark-util-normalize-identifier": "2.0.0", "micromark-util-resolve-all": "2.0.0", "micromark-util-subtokenize": "2.0.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", { "dependencies": { "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", "micromark-extension-gfm-strikethrough": "2.1.0", "micromark-extension-gfm-table": "2.1.1", "micromark-extension-gfm-tagfilter": "2.0.0", "micromark-extension-gfm-task-list-item": "2.1.0", "micromark-util-combine-extensions": "2.0.0", "micromark-util-types": "2.0.2" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-sanitize-uri": "2.0.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-core-commonmark": "2.0.3", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-normalize-identifier": "2.0.0", "micromark-util-sanitize-uri": "2.0.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-util-chunked": "2.0.0", "micromark-util-classify-character": "2.0.1", "micromark-util-resolve-all": "2.0.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", { "dependencies": { "micromark-util-types": "2.0.2" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "https://registry.npmmirror.com/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", { "dependencies": { "@types/katex": "0.16.8", "devlop": "1.1.0", "katex": "0.16.47", "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.0", "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.0", "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-types": "2.0.2" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.0", "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", { "dependencies": { "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.0", "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", { "dependencies": { "micromark-factory-space": "2.0.1", "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", { "dependencies": { "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.0", "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", { "dependencies": { "micromark-util-symbol": "2.0.1" } }, "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.0", "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", { "dependencies": { "micromark-util-chunked": "2.0.0", "micromark-util-types": "2.0.2" } }, "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.0", "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.0.tgz", { "dependencies": { "micromark-util-symbol": "2.0.1" } }, "sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", { "dependencies": { "decode-named-character-reference": "1.3.0", "micromark-util-character": "2.1.1", "micromark-util-decode-numeric-character-reference": "2.0.0", "micromark-util-symbol": "2.0.1" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.0", "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", {}, "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.0", "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", {}, "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.0", "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", { "dependencies": { "micromark-util-symbol": "2.0.1" } }, "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.0", "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", { "dependencies": { "micromark-util-types": "2.0.2" } }, "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", { "dependencies": { "micromark-util-character": "2.1.1", "micromark-util-encode": "2.0.0", "micromark-util-symbol": "2.0.1" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.0.0", "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", { "dependencies": { "devlop": "1.1.0", "micromark-util-chunked": "2.0.0", "micromark-util-symbol": "2.0.1", "micromark-util-types": "2.0.2" } }, "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "ms": ["ms@2.0.0", "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "nanoid": ["nanoid@3.3.18", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "negotiator": ["negotiator@1.1.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.1.0.tgz", { "dependencies": { "content-type": "2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "node-addon-api": ["node-addon-api@7.1.0", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.0.tgz", {}, "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g=="], + + "node-pty": ["node-pty@1.1.0", "https://registry.npmmirror.com/node-pty/-/node-pty-1.1.0.tgz", { "dependencies": { "node-addon-api": "7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="], + + "obug": ["obug@2.1.4", "https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], + + "on-headers": ["on-headers@1.1.0", "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", { "dependencies": { "oniguruma-parser": "0.12.2", "regex": "6.1.0", "regex-recursion": "6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "package-manager-detector": ["package-manager-detector@1.8.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.8.0.tgz", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], + + "parse5": ["parse5@8.0.1", "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", { "dependencies": { "entities": "8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + + "path-data-parser": ["path-data-parser@0.1.0", "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + + "pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "playwright": ["playwright@1.62.1", "https://registry.npmmirror.com/playwright/-/playwright-1.62.1.tgz", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], + + "playwright-core": ["playwright-core@1.62.1", "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.62.1.tgz", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], + + "points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + + "postcss": ["postcss@8.5.26", "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", { "dependencies": { "nanoid": "3.3.18", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "property-information": ["property-information@7.2.0", "https://registry.npmmirror.com/property-information/-/property-information-7.2.0.tgz", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "quansync": ["quansync@1.0.0", "https://registry.npmmirror.com/quansync/-/quansync-1.0.0.tgz", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], + + "react": ["react@18.2.0", "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", { "dependencies": { "loose-envify": "1.4.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + + "react-dom": ["react-dom@18.2.0", "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", { "dependencies": { "loose-envify": "1.4.0", "scheduler": "0.23.0" }, "peerDependencies": { "react": "18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="], + + "react-icons": ["react-icons@5.7.0", "https://registry.npmmirror.com/react-icons/-/react-icons-5.7.0.tgz", { "peerDependencies": { "react": "18.2.0" } }, "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw=="], + + "regex": ["regex@6.1.0", "https://registry.npmmirror.com/regex/-/regex-6.1.0.tgz", { "dependencies": { "regex-utilities": "2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz", { "dependencies": { "regex-utilities": "2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + + "rolldown": ["rolldown@1.2.5", "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.5.tgz", { "dependencies": { "@oxc-project/types": "0.146.0", "@rolldown/pluginutils": "1.0.1" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], + + "rolldown-plugin-dts": ["rolldown-plugin-dts@0.27.14", "https://registry.npmmirror.com/rolldown-plugin-dts/-/rolldown-plugin-dts-0.27.14.tgz", { "dependencies": { "dts-resolver": "3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "2.1.4", "yuku-ast": "0.8.7", "yuku-codegen": "0.8.7", "yuku-parser": "0.8.7" }, "optionalDependencies": { "typescript": "5.6.2" }, "peerDependencies": { "rolldown": "1.2.5" } }, "sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw=="], + + "roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "0.5.2", "path-data-parser": "0.1.0", "points-on-curve": "0.2.0", "points-on-path": "0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + + "rw": ["rw@1.3.3", "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "rxjs": ["rxjs@7.8.2", "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", { "dependencies": { "xmlchars": "2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.23.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", { "dependencies": { "loose-envify": "1.4.0" } }, "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw=="], + + "schemastery": ["schemastery@3.18.0", "https://registry.npmmirror.com/schemastery/-/schemastery-3.18.0.tgz", { "dependencies": { "@standard-schema/spec": "1.1.0", "cosmokit": "1.8.1" } }, "sha512-Jw2uxjoyyqc/yeurmChUEc/jbi8GsrdXV/KmqRUDZXJAXAmrJiPsz8vKa17l/VckyzljHZ9oGaul443CQiXxtA=="], + + "shiki": ["shiki@4.4.3", "https://registry.npmmirror.com/shiki/-/shiki-4.4.3.tgz", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "10.0.2", "@types/hast": "3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="], + + "siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "https://registry.npmmirror.com/std-env/-/std-env-4.2.0.tgz", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "strictdom": ["strictdom@1.0.1", "https://registry.npmmirror.com/strictdom/-/strictdom-1.0.1.tgz", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="], + + "stringify-entities": ["stringify-entities@4.0.4", "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", { "dependencies": { "character-entities-html4": "2.1.0", "character-entities-legacy": "3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-mod": ["style-mod@4.1.3", "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.3.tgz", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + + "stylis": ["stylis@4.4.0", "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + + "symbol-tree": ["symbol-tree@3.2.4", "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.3.0", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.5" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinyrainbow": ["tinyrainbow@3.1.1", "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.1.tgz", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], + + "tldts": ["tldts@7.4.10", "https://registry.npmmirror.com/tldts/-/tldts-7.4.10.tgz", { "dependencies": { "tldts-core": "7.4.10" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog=="], + + "tldts-core": ["tldts-core@7.4.10", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.10.tgz", {}, "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw=="], + + "tough-cookie": ["tough-cookie@6.0.2", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.2.tgz", { "dependencies": { "tldts": "7.4.10" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", { "dependencies": { "punycode": "2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + + "tree-kill": ["tree-kill@1.2.2", "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "ts-dedent": ["ts-dedent@2.3.0", "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.3.0.tgz", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], + + "tsdown": ["tsdown@0.22.14", "https://registry.npmmirror.com/tsdown/-/tsdown-0.22.14.tgz", { "dependencies": { "ansis": "4.3.1", "cac": "7.0.0", "defu": "6.1.7", "empathic": "2.0.1", "hookable": "6.1.1", "import-without-cache": "0.4.0", "obug": "2.1.4", "picomatch": "4.0.5", "rolldown": "1.2.5", "rolldown-plugin-dts": "0.27.14", "tinyexec": "1.3.0", "tinyglobby": "0.2.17", "tree-kill": "1.2.2", "unconfig-core": "7.5.0", "verkit": "0.3.2" }, "optionalDependencies": { "typescript": "5.6.2", "unrun": "0.2.39" }, "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-ule7Y+fsAN2iZbLDoo7C4KYljFJNJJ+fLshyn+9gozeTspVersWHxwdGB+Dm2hzA38s6muFnUTl0jK3vJm9ifQ=="], + + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.6.2", "https://registry.npmmirror.com/typescript/-/typescript-5.6.2.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw=="], + + "unconfig-core": ["unconfig-core@7.5.0", "https://registry.npmmirror.com/unconfig-core/-/unconfig-core-7.5.0.tgz", { "dependencies": { "@quansync/fs": "1.0.0", "quansync": "1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], + + "undici": ["undici@7.29.0", "https://registry.npmmirror.com/undici/-/undici-7.29.0.tgz", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "unist-util-is": ["unist-util-is@6.0.1", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "https://registry.npmmirror.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", { "dependencies": { "@types/unist": "3.0.3", "unist-util-visit": "5.1.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", { "dependencies": { "@types/unist": "3.0.3" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", { "dependencies": { "@types/unist": "3.0.3", "unist-util-is": "6.0.1", "unist-util-visit-parents": "6.0.2" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", { "dependencies": { "@types/unist": "3.0.3", "unist-util-is": "6.0.1" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "unrun": ["unrun@0.2.39", "https://registry.npmmirror.com/unrun/-/unrun-0.2.39.tgz", { "dependencies": { "rolldown": "1.0.0-rc.17" }, "bin": { "unrun": "dist/cli.mjs" } }, "sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg=="], + + "uuid": ["uuid@14.0.2", "https://registry.npmmirror.com/uuid/-/uuid-14.0.2.tgz", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="], + + "vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "verkit": ["verkit@0.3.2", "https://registry.npmmirror.com/verkit/-/verkit-0.3.2.tgz", {}, "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg=="], + + "vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "3.0.3", "vfile-message": "4.0.3" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "3.0.3", "unist-util-stringify-position": "4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@8.2.2", "https://registry.npmmirror.com/vite/-/vite-8.2.2.tgz", { "dependencies": { "lightningcss": "1.33.0", "picomatch": "4.0.5", "postcss": "8.5.26", "rolldown": "1.2.5", "tinyglobby": "0.2.17" }, "optionalDependencies": { "@types/node": "24.13.3", "fsevents": "2.3.3" }, "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], + + "vitest": ["vitest@4.1.11", "https://registry.npmmirror.com/vitest/-/vitest-4.1.11.tgz", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "2.3.2", "expect-type": "1.4.0", "magic-string": "0.30.21", "obug": "2.1.4", "pathe": "2.0.3", "picomatch": "4.0.5", "std-env": "4.2.0", "tinybench": "2.9.0", "tinyexec": "1.3.0", "tinyglobby": "0.2.17", "tinyrainbow": "3.1.1", "why-is-node-running": "2.3.0" }, "optionalDependencies": { "@types/node": "24.13.3", "jsdom": "29.1.1" }, "peerDependencies": { "vite": "8.2.2" }, "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@16.0.1", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", { "dependencies": { "@exodus/bytes": "1.15.1", "tr46": "6.0.0", "webidl-conversions": "8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "ws": ["ws@8.21.3", "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz", {}, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yuku-ast": ["yuku-ast@0.8.7", "https://registry.npmmirror.com/yuku-ast/-/yuku-ast-0.8.7.tgz", { "dependencies": { "@yuku-toolchain/types": "0.8.7" } }, "sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ=="], + + "yuku-codegen": ["yuku-codegen@0.8.7", "https://registry.npmmirror.com/yuku-codegen/-/yuku-codegen-0.8.7.tgz", { "dependencies": { "@yuku-toolchain/types": "0.8.7" }, "optionalDependencies": { "@yuku-codegen/binding-android-arm64": "0.8.7", "@yuku-codegen/binding-darwin-arm64": "0.8.7", "@yuku-codegen/binding-darwin-x64": "0.8.7", "@yuku-codegen/binding-freebsd-x64": "0.8.7", "@yuku-codegen/binding-linux-arm-gnu": "0.8.7", "@yuku-codegen/binding-linux-arm-musl": "0.8.7", "@yuku-codegen/binding-linux-arm64-gnu": "0.8.7", "@yuku-codegen/binding-linux-arm64-musl": "0.8.7", "@yuku-codegen/binding-linux-x64-gnu": "0.8.7", "@yuku-codegen/binding-linux-x64-musl": "0.8.7", "@yuku-codegen/binding-win32-arm64": "0.8.7", "@yuku-codegen/binding-win32-x64": "0.8.7" } }, "sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw=="], + + "yuku-parser": ["yuku-parser@0.8.7", "https://registry.npmmirror.com/yuku-parser/-/yuku-parser-0.8.7.tgz", { "dependencies": { "@yuku-toolchain/types": "0.8.7", "yuku-ast": "0.8.7" }, "optionalDependencies": { "@yuku-parser/binding-android-arm64": "0.8.7", "@yuku-parser/binding-darwin-arm64": "0.8.7", "@yuku-parser/binding-darwin-x64": "0.8.7", "@yuku-parser/binding-freebsd-x64": "0.8.7", "@yuku-parser/binding-linux-arm-gnu": "0.8.7", "@yuku-parser/binding-linux-arm-musl": "0.8.7", "@yuku-parser/binding-linux-arm64-gnu": "0.8.7", "@yuku-parser/binding-linux-arm64-musl": "0.8.7", "@yuku-parser/binding-linux-x64-gnu": "0.8.7", "@yuku-parser/binding-linux-x64-musl": "0.8.7", "@yuku-parser/binding-win32-arm64": "0.8.7", "@yuku-parser/binding-win32-x64": "0.8.7" } }, "sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ=="], + + "zod": ["zod@4.4.3", "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@deepseek-ai/dsh-system-prompt/@deepseek-ai/schemastery": ["@deepseek-ai/schemastery@3.18.1", "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.2", "@standard-schema/spec": "1.1.0" } }, "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg=="], + + "@deepseek-ai/dsh-user-approval/@deepseek-ai/schemastery": ["@deepseek-ai/schemastery@3.18.1", "https://registry.npmmirror.com/@deepseek-ai/schemastery/-/schemastery-3.18.1.tgz", { "dependencies": { "@deepseek-ai/cosmokit": "1.8.2", "@standard-schema/spec": "1.1.0" } }, "sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg=="], + + "@deepseek-ai/schemastery/@deepseek-ai/cosmokit": ["@deepseek-ai/cosmokit@1.8.3", "https://registry.npmmirror.com/@deepseek-ai/cosmokit/-/cosmokit-1.8.3.tgz", {}, "sha512-qBo+ronVM6Eu2WNVJXi8JcMiqZ19T9BRIpV+5qJUFPXjGH/Z0QKcQMC/IZJ7L394YTOtJgcovbk9qP0w2GsBXQ=="], + + "compression/negotiator": ["negotiator@0.6.4", "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", { "dependencies": { "layout-base": "2.0.1" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.0.0", "https://registry.npmmirror.com/commander/-/commander-7.0.0.tgz", {}, "sha512-ovx/7NkTrnPuIV8sqk/GjUIIM1+iUQeqA3ye2VNpq9sVoiZsooObWlQy+OPWGI17GDaEoybuAGJm6U8yC077BA=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", { "dependencies": { "internmap": "1.0.1" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", { "dependencies": { "d3-path": "1.0.9" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "mdast-util-math/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", { "dependencies": { "@types/mdast": "4.0.4", "@types/unist": "3.0.3", "longest-streak": "3.1.0", "mdast-util-phrasing": "4.1.0", "mdast-util-to-string": "4.0.0", "micromark-util-decode-string": "2.0.1", "unist-util-visit": "5.1.0", "zwitch": "2.0.4" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + + "micromark/debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "unrun/rolldown": ["rolldown@1.0.0-rc.17", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.17.tgz", { "dependencies": { "@oxc-project/types": "0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], + + "vite/fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + + "micromark/debug/ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.127.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.127.0.tgz", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], + + "unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="], + + "unrun/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="], + + "unrun/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="], + + "unrun/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="], + + "unrun/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="], + + "unrun/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="], + + "unrun/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="], + + "unrun/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="], + + "unrun/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="], + + "unrun/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="], + + "unrun/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="], + + "unrun/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="], + + "unrun/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="], + + "unrun/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="], + + "unrun/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], + } +} diff --git a/package.json b/package.json index 036a3adf9..c5a6e732b 100644 --- a/package.json +++ b/package.json @@ -170,5 +170,8 @@ "typescript": "^5.6.0", "unrun": "^0.2.39", "vitest": "^4.1.8" - } + }, + "workspaces": [ + "." + ] } From aa5bdfb8c6ba617cd509e58d8957cbb8322fabe8 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Sat, 5 Sep 2026 19:27:15 +0800 Subject: [PATCH 08/11] feat: default expand diff view and click tool to jump to subagent - Diff view: default expand single-file diffs and all files in DiffTab (with wider code/config file extension matching) - Subagent navigation: directly jump to corresponding child subagent session when clicking subagent tool rows in chat --- src/client/DiffTab.tsx | 4 +- src/client/SideChatView.tsx | 7 +- src/client/diff/DiffFiles.tsx | 25 ++- src/client/index.tsx | 13 ++ src/client/layout.css | 19 +++ src/client/subagent-tool-jump.ts | 186 +++++++++++++++++++++ tests/diff-files-collapse.spec.tsx | 42 +++++ tests/subagent-tool-jump.spec.ts | 259 +++++++++++++++++++++++++++++ 8 files changed, 545 insertions(+), 10 deletions(-) create mode 100644 src/client/subagent-tool-jump.ts create mode 100644 tests/subagent-tool-jump.spec.ts diff --git a/src/client/DiffTab.tsx b/src/client/DiffTab.tsx index 9f03e7b93..937459b3d 100644 --- a/src/client/DiffTab.tsx +++ b/src/client/DiffTab.tsx @@ -103,8 +103,8 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif {!loading && error === null && data !== null && ( <> {data.untracked !== undefined - ? - : } + ? + : } {data.diff === '' && data.untracked === undefined && (
{t('diffEmpty')}
)} diff --git a/src/client/SideChatView.tsx b/src/client/SideChatView.tsx index 367ee8830..77d960afa 100644 --- a/src/client/SideChatView.tsx +++ b/src/client/SideChatView.tsx @@ -178,7 +178,10 @@ function CollapsibleRow(props: { : null if (props.children === undefined) { return ( -
+
{leading} {label} {meta} @@ -186,7 +189,7 @@ function CollapsibleRow(props: { ) } return ( -
+
{ +/** + * Source files open by default; tests, docs, generated files and unknown types stay folded. + * When `expandAll` is true or there is only a single file in the diff, all expandable files open by default. + */ +function defaultExpandedFiles(files: readonly DiffFile[], expandAll?: boolean): Set { const expanded = new Set() + if (expandAll || files.length === 1) { + files.forEach((file, index) => { + if (!file.binary && file.hunks.length > 0) { + expanded.add(index) + } + }) + return expanded + } files.forEach((file, index) => { const path = displayPath(file.newPath === '/dev/null' ? file.oldPath : file.newPath) if (!file.binary && file.hunks.length > 0 @@ -51,17 +62,19 @@ export interface DiffFilesProps { /** Untracked-file content: when present, renders as a full-file addition instead of parsing. */ untrackedPath?: string untrackedContent?: string + /** Whether all expandable files start expanded (defaults to false; single-file diffs always expand). */ + defaultExpandAll?: boolean } -export function DiffFiles({ diff, untrackedPath, untrackedContent }: DiffFilesProps) { +export function DiffFiles({ diff, untrackedPath, untrackedContent, defaultExpandAll }: DiffFilesProps) { const parsed = useMemo(() => { if (untrackedPath !== undefined) { return { files: [untrackedFile(untrackedPath, untrackedContent ?? '')] } } return parseUnifiedDiff(diff) }, [diff, untrackedPath, untrackedContent]) - const [expandedFiles, setExpandedFiles] = useState>(() => defaultExpandedFiles(parsed.files)) - useEffect(() => { setExpandedFiles(defaultExpandedFiles(parsed.files)) }, [parsed]) + const [expandedFiles, setExpandedFiles] = useState>(() => defaultExpandedFiles(parsed.files, defaultExpandAll)) + useEffect(() => { setExpandedFiles(defaultExpandedFiles(parsed.files, defaultExpandAll)) }, [parsed, defaultExpandAll]) // Segments and header stats computed once per file. const files = useMemo( diff --git a/src/client/index.tsx b/src/client/index.tsx index aed2ee816..9b1f67a6d 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -19,6 +19,7 @@ import { Sidebar } from './Sidebar.tsx' import { RenderBoundary } from './RenderBoundary.tsx' import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx' import { registerLinkInterception } from './link-intercept.ts' +import { registerSubagentToolJump } from './subagent-tool-jump.ts' import { registerImeGuard } from './ime-guard.ts' import { registerSettingsNavIcon } from './settings-nav-icon.ts' import { loadBootDecision } from './prefs.ts' @@ -398,6 +399,18 @@ export function apply(ctx: Context): void { 'dsh-better-sidebar: link interception', ) + ctx.effect( + () => { + try { + return registerSubagentToolJump(ctx, sidebarStore) + } catch (error) { + fail('subagent jump interception', error) + return () => {} + } + }, + 'dsh-better-sidebar: subagent tool jump interception', + ) + // The IME guard: composition keys (candidate arrows, confirm, cancel) // belong to the input method, never to page JS. Inlined third-party UI // (formerly Univer's office controls, #562 regression) has shipped diff --git a/src/client/layout.css b/src/client/layout.css index f497840da..146cf2b63 100644 --- a/src/client/layout.css +++ b/src/client/layout.css @@ -171,3 +171,22 @@ body[data-dsh-sidebar-submenu~="left"] div[role="menu"] div[role="menu"]::before left: auto; right: -4px; } + +/* Subagent tool-call visual affordance: indicate click-to-jump to subagent */ +[data-tool^="subagent"], +[data-tool="send_message"], +[data-tool="interrupt_agent"] { + cursor: pointer; +} + +[data-tool^="subagent"] [class*="summary"], +[data-tool="send_message"] [class*="summary"] { + cursor: pointer; + transition: color 0.15s ease; +} + +[data-tool^="subagent"]:hover [class*="summary"], +[data-tool="send_message"]:hover [class*="summary"] { + color: var(--dsw-alias-interactive-text-hover, inherit); + text-decoration: underline dotted; +} diff --git a/src/client/subagent-tool-jump.ts b/src/client/subagent-tool-jump.ts new file mode 100644 index 000000000..e7ce27b41 --- /dev/null +++ b/src/client/subagent-tool-jump.ts @@ -0,0 +1,186 @@ +/** + * Subagent tool-call jump interception: + * Clicking a subagent delegation tool call (e.g. `subagent`, `subagent_explorer`, + * `subagent_fixer`, `subagent_oracle`, `subagent_librarian`, `subagent_designer`, + * `subagent_council`, `subagent_fork`, `send_message`, etc.) in the main chat + * or sidechat navigates directly to the corresponding child subagent session. + */ +import type { + Context, + SidebarSessionList, + SidebarSubagentAddress, + SidebarSubagentChildEntry, +} from '../context-types.ts' +import { t } from './locales.ts' +import { isPlainLeftClick } from './link-intercept.ts' +import { firstLeaf, togglePanel, type SidebarStore } from './state.ts' + +/** Whether a tool name represents a subagent delegation or communication tool. */ +export function isSubagentTool(name: string | null | undefined): boolean { + if (!name) return false + const trimmed = name.trim() + return ( + trimmed === 'subagent' || + trimmed.startsWith('subagent_') || + trimmed === 'send_message' || + trimmed === 'interrupt_agent' + ) +} + +/** + * Find the corresponding child subagent session id for a given tool element. + * @param toolEl - the DOM element containing or tagged with the tool call + * @param parentSessionId - current active parent session id + * @param sessionList - snapshot of all sessions + * @returns child session id, if identified + */ +export function findSubagentForTool( + toolEl: Element, + parentSessionId: string, + sessionList: SidebarSessionList, +): string | undefined { + const text = toolEl.textContent ?? '' + + // 1. Check for explicit UUID match in the tool element text (e.g. in result or args) + const uuidMatches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) + if (uuidMatches) { + for (const id of uuidMatches) { + if (sessionList.byId[id]?.parentId === parentSessionId) { + return id + } + } + } + + // 2. Check for explicit agent id pattern (e.g. `started subagent a-...` or `agent_id: "..."`) + const idMatch = text.match(/\b(a-[a-zA-Z0-9_-]+)\b/) + if (idMatch && idMatch[1] !== undefined && sessionList.byId[idMatch[1]]) { + return idMatch[1] + } + + // 3. Match by child label from subagentsByParent catalog + const catalog = sessionList.subagentsByParent?.[parentSessionId] + const catalogChildren = (catalog?.entries ?? []).filter( + (entry): entry is SidebarSubagentChildEntry => entry.kind === 'child', + ) + for (const child of catalogChildren) { + if (child.label && child.label.trim() !== '' && text.includes(child.label.trim())) { + return child.id + } + } + + // 4. Match by displayTitle from sessionList.byId + const directChildren = Object.values(sessionList.byId).filter( + summary => summary.origin === 'subagent' && summary.parentId === parentSessionId, + ) + for (const child of directChildren) { + if (child.displayTitle && child.displayTitle.trim() !== '' && text.includes(child.displayTitle.trim())) { + return child.id + } + } + + // 5. If there is only one child, it is unambiguously that child + if (catalogChildren.length === 1 && catalogChildren[0] !== undefined) { + return catalogChildren[0].id + } + if (directChildren.length === 1 && directChildren[0] !== undefined) { + return directChildren[0].id + } + + // 6. Match by structural DOM position among all subagent tools in the document + if (catalogChildren.length > 0 || directChildren.length > 0) { + const allTools = Array.from(document.querySelectorAll('[data-tool]')).filter(el => + isSubagentTool(el.getAttribute('data-tool')), + ) + const index = allTools.indexOf(toolEl) + if (index >= 0) { + const childFromCatalog = catalogChildren[index] + if (childFromCatalog !== undefined) return childFromCatalog.id + const childFromDirect = directChildren[index] + if (childFromDirect !== undefined) return childFromDirect.id + } + } + + // 7. Fallback to latest child created + const lastCatalogChild = catalogChildren[catalogChildren.length - 1] + if (lastCatalogChild !== undefined) return lastCatalogChild.id + const lastDirectChild = directChildren[directChildren.length - 1] + return lastDirectChild?.id +} + +/** + * Navigate to the target subagent child session and highlight it in the sidebar. + */ +export function jumpToSubagent( + ctx: Context, + store: SidebarStore, + parentSessionId: string, + childSessionId: string, + onJump?: (id: string) => void, +): void { + onJump?.(childSessionId) + const sessions = ctx.sessions + try { + if (typeof sessions?.openSubagent === 'function') { + const address: SidebarSubagentAddress = sessions.subagentAddress?.(childSessionId) ?? { + parentSessionId, + childSessionId, + mode: 'one-shot', + } + sessions.openSubagent(address) + } else if (typeof sessions?.open === 'function') { + sessions.open(childSessionId) + } + } catch (error) { + console.error('[dsh-better-sidebar] Failed to open subagent session:', error) + } + + // Ensure the sidebar panel is expanded and Tasks tab is active + store.reduce(s => (s.panelOpen ? s : togglePanel(s))) + store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id })) + ctx.get('betterSidebar')?.openTab({ type: 'subagent', title: t('subagent') }) +} + +/** + * Register document-level click interception to navigate to subagent on tool-call clicks. + * @returns cleanup disposer (HMR-safe) + */ +export function registerSubagentToolJump( + ctx: Context, + store: SidebarStore, + onJump?: (id: string) => void, +): () => void { + const onClick = (event: MouseEvent): void => { + if (!isPlainLeftClick(event)) return + if (event.defaultPrevented) return + + const target = event.target as Element | null + if (!target || typeof target.closest !== 'function') return + + // Find closest tool call container + const toolEl = target.closest('[data-tool]') + if (!toolEl) return + + const toolName = toolEl.getAttribute('data-tool') + if (!isSubagentTool(toolName)) return + + // If user explicitly clicked a toggle chevron, let it toggle expansion + const isChevron = Boolean(target.closest('[class*="chevron" i], [data-slot*="chevron" i], svg')) + if (isChevron) return + + const snapshot = ctx.sessions?.list?.getSnapshot() + const parentSessionId = snapshot?.current + if (!parentSessionId) return + + const childId = findSubagentForTool(toolEl, parentSessionId, snapshot) + if (!childId) return + + event.preventDefault() + event.stopPropagation() + jumpToSubagent(ctx, store, parentSessionId, childId, onJump) + } + + document.addEventListener('click', onClick, true) + return () => { + document.removeEventListener('click', onClick, true) + } +} diff --git a/tests/diff-files-collapse.spec.tsx b/tests/diff-files-collapse.spec.tsx index 408c67016..434b498db 100644 --- a/tests/diff-files-collapse.spec.tsx +++ b/tests/diff-files-collapse.spec.tsx @@ -57,4 +57,46 @@ describe('DiffFiles file folding', () => { container.remove() } }) + + it('expands single file diff by default even if it is a test file', () => { + const singleTestDiff = [ + 'diff --git a/tests/b.spec.ts b/tests/b.spec.ts', + '--- a/tests/b.spec.ts', + '+++ b/tests/b.spec.ts', + '@@ -1 +1 @@', + '-old-b', + '+new-b', + ].join('\n') + const container = document.createElement('div') + document.body.append(container) + const root: Root = createRoot(container) + try { + act(() => { root.render(createElement(DiffFiles, { diff: singleTestDiff })) }) + const headers = [...container.querySelectorAll('button[aria-expanded]')] + expect(headers).toHaveLength(1) + expect(headers[0]!.getAttribute('aria-expanded')).toBe('true') + expect(container.textContent).toContain('new-b') + } finally { + act(() => { root.unmount() }) + container.remove() + } + }) + + it('expands all files when defaultExpandAll is true', () => { + const container = document.createElement('div') + document.body.append(container) + const root: Root = createRoot(container) + try { + act(() => { root.render(createElement(DiffFiles, { diff, defaultExpandAll: true })) }) + const headers = [...container.querySelectorAll('button[aria-expanded]')] + expect(headers).toHaveLength(3) + expect(headers.map(header => header.getAttribute('aria-expanded'))).toEqual(['true', 'true', 'true']) + expect(container.textContent).toContain('new-a') + expect(container.textContent).toContain('new-b') + expect(container.textContent).toContain('new-doc') + } finally { + act(() => { root.unmount() }) + container.remove() + } + }) }) diff --git a/tests/subagent-tool-jump.spec.ts b/tests/subagent-tool-jump.spec.ts new file mode 100644 index 000000000..aedc83a7b --- /dev/null +++ b/tests/subagent-tool-jump.spec.ts @@ -0,0 +1,259 @@ +// @vitest-environment jsdom +import { describe, expect, it, vi, afterEach } from 'vitest' +import { + isSubagentTool, + findSubagentForTool, + jumpToSubagent, + registerSubagentToolJump, +} from '../src/client/subagent-tool-jump.ts' +import type { Context, SidebarSessionList } from '../src/context-types.ts' +import type { SidebarStore } from '../src/client/state.ts' + +afterEach(() => { + document.body.innerHTML = '' +}) + +describe('isSubagentTool', () => { + it('returns true for subagent delegation and control tools', () => { + expect(isSubagentTool('subagent')).toBe(true) + expect(isSubagentTool('subagent_explorer')).toBe(true) + expect(isSubagentTool('subagent_fixer')).toBe(true) + expect(isSubagentTool('subagent_oracle')).toBe(true) + expect(isSubagentTool('subagent_librarian')).toBe(true) + expect(isSubagentTool('subagent_designer')).toBe(true) + expect(isSubagentTool('subagent_councillor_alpha')).toBe(true) + expect(isSubagentTool('subagent_council')).toBe(true) + expect(isSubagentTool('subagent_fork')).toBe(true) + expect(isSubagentTool('subagent_anything')).toBe(true) + expect(isSubagentTool('send_message')).toBe(true) + expect(isSubagentTool('interrupt_agent')).toBe(true) + }) + + it('returns false for non-subagent tools or null/undefined', () => { + expect(isSubagentTool('bash')).toBe(false) + expect(isSubagentTool('read')).toBe(false) + expect(isSubagentTool('write')).toBe(false) + expect(isSubagentTool('edit')).toBe(false) + expect(isSubagentTool('glob')).toBe(false) + expect(isSubagentTool('')).toBe(false) + expect(isSubagentTool(null)).toBe(false) + expect(isSubagentTool(undefined)).toBe(false) + }) +}) + +describe('findSubagentForTool', () => { + const parentId = 'parent-123' + const child1Id = '11111111-2222-3333-4444-555555555555' + const child2Id = '22222222-3333-4444-5555-666666666666' + + const mockSessionList: SidebarSessionList = { + current: parentId, + byId: { + [parentId]: { id: parentId, displayTitle: 'Main Parent' }, + [child1Id]: { + id: child1Id, + parentId, + displayTitle: 'Explorer Task', + origin: 'subagent', + }, + [child2Id]: { + id: child2Id, + parentId, + displayTitle: 'Fixer Task', + origin: 'subagent', + }, + }, + subagentsByParent: { + [parentId]: { + entries: [ + { + kind: 'child', + id: child1Id, + label: 'Search codebase for models', + activity: 'inactive', + hasChildren: false, + mode: 'one-shot', + }, + { + kind: 'child', + id: child2Id, + label: 'Implement fix for diff view', + activity: 'running', + hasChildren: false, + mode: 'one-shot', + }, + ], + parentAvailable: true, + state: 'ready', + }, + }, + } + + it('matches child by explicit UUID in tool element text', () => { + const el = document.createElement('div') + el.textContent = `Tool finished for child ${child1Id} successfully` + expect(findSubagentForTool(el, parentId, mockSessionList)).toBe(child1Id) + }) + + it('matches child by catalog entry label', () => { + const el = document.createElement('div') + el.textContent = 'subagent_explorer · Search codebase for models' + expect(findSubagentForTool(el, parentId, mockSessionList)).toBe(child1Id) + + const el2 = document.createElement('div') + el2.textContent = 'subagent_fixer · Implement fix for diff view' + expect(findSubagentForTool(el2, parentId, mockSessionList)).toBe(child2Id) + }) + + it('matches child by displayTitle in byId', () => { + const el = document.createElement('div') + el.textContent = 'subagent_explorer · Explorer Task' + expect(findSubagentForTool(el, parentId, mockSessionList)).toBe(child1Id) + }) + + it('resolves unambiguously when there is only one child', () => { + const singleList: SidebarSessionList = { + current: parentId, + byId: { + [parentId]: { id: parentId, displayTitle: 'Main Parent' }, + [child1Id]: { id: child1Id, parentId, displayTitle: 'Child 1', origin: 'subagent' }, + }, + subagentsByParent: { + [parentId]: { + entries: [ + { kind: 'child', id: child1Id, label: 'Single Child', activity: 'running', hasChildren: false, mode: 'one-shot' }, + ], + parentAvailable: true, + state: 'ready', + }, + }, + } + const el = document.createElement('div') + el.textContent = 'subagent · some generic prompt' + expect(findSubagentForTool(el, parentId, singleList)).toBe(child1Id) + }) +}) + +describe('jumpToSubagent', () => { + it('calls ctx.sessions.openSubagent with parent and child ids', () => { + const openSubagent = vi.fn() + const openTab = vi.fn() + const reduce = vi.fn() + const ctx = { + sessions: { openSubagent }, + get: vi.fn(() => ({ openTab })), + } as unknown as Context + const store = { reduce } as unknown as SidebarStore + + jumpToSubagent(ctx, store, 'p-1', 'c-1') + + expect(openSubagent).toHaveBeenCalledWith({ + parentSessionId: 'p-1', + childSessionId: 'c-1', + mode: 'one-shot', + }) + expect(openTab).toHaveBeenCalledWith(expect.objectContaining({ type: 'subagent' })) + }) + + it('falls back to ctx.sessions.open when openSubagent is missing', () => { + const open = vi.fn() + const openTab = vi.fn() + const ctx = { + sessions: { open }, + get: vi.fn(() => ({ openTab })), + } as unknown as Context + const store = { reduce: vi.fn() } as unknown as SidebarStore + + jumpToSubagent(ctx, store, 'p-1', 'c-1') + + expect(open).toHaveBeenCalledWith('c-1') + }) +}) + +describe('registerSubagentToolJump', () => { + it('intercepts clicks on subagent tools and calls openSubagent', () => { + const openSubagent = vi.fn() + const openTab = vi.fn() + const ctx = { + sessions: { + openSubagent, + list: { + getSnapshot: () => ({ + current: 'p-1', + byId: { + 'p-1': { id: 'p-1', displayTitle: 'Parent' }, + 'c-1': { id: 'c-1', parentId: 'p-1', displayTitle: 'Subagent 1', origin: 'subagent' }, + }, + subagentsByParent: { + 'p-1': { + entries: [ + { kind: 'child', id: 'c-1', label: 'Explore Code', activity: 'inactive', hasChildren: false, mode: 'one-shot' }, + ], + parentAvailable: true, + state: 'ready', + }, + }, + }), + }, + }, + get: vi.fn(() => ({ openTab })), + } as unknown as Context + const store = { reduce: vi.fn() } as unknown as SidebarStore + + const dispose = registerSubagentToolJump(ctx, store) + + const toolEl = document.createElement('div') + toolEl.setAttribute('data-tool', 'subagent_explorer') + const summarySpan = document.createElement('span') + summarySpan.className = 'summary' + summarySpan.textContent = 'Explore Code' + toolEl.appendChild(summarySpan) + document.body.appendChild(toolEl) + + summarySpan.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 }), + ) + + expect(openSubagent).toHaveBeenCalledWith({ + parentSessionId: 'p-1', + childSessionId: 'c-1', + mode: 'one-shot', + }) + + dispose() + }) + + it('allows clicking chevron without triggering jump', () => { + const openSubagent = vi.fn() + const ctx = { + sessions: { + openSubagent, + list: { + getSnapshot: () => ({ + current: 'p-1', + byId: {}, + }), + }, + }, + get: vi.fn(), + } as unknown as Context + const store = { reduce: vi.fn() } as unknown as SidebarStore + + const dispose = registerSubagentToolJump(ctx, store) + + const toolEl = document.createElement('div') + toolEl.setAttribute('data-tool', 'subagent_fixer') + const chevron = document.createElement('span') + chevron.className = 'toolChevron' + toolEl.appendChild(chevron) + document.body.appendChild(toolEl) + + chevron.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 }), + ) + + expect(openSubagent).not.toHaveBeenCalled() + + dispose() + }) +}) From 79ebee5f29b80be9ab42276b16a43cb0f68b66e7 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Sat, 5 Sep 2026 21:18:30 +0800 Subject: [PATCH 09/11] fix(editor): open read-tool file links as plain file instead of diff - Read tool clicks (or file links in read rows) always open as plain file in editor, never probing or showing git diff - Automatically jump and scroll to the read line/offset in the editor when available - Preserve git diff preview for actual edit/write tool links via editOpensDiff --- src/client/EditorHost.tsx | 1 + src/client/TextEditor.tsx | 16 +++- src/client/chat-preview.ts | 4 +- src/client/index.tsx | 13 +++ src/client/intercept.tsx | 39 +++++++- src/client/service.ts | 2 + src/client/subagent-tool-jump.ts | 32 ++++++- src/client/tool-click-context.ts | 149 +++++++++++++++++++++++++++++++ tests/openpath-intercept.spec.ts | 49 ++++++++++ tests/subagent-tool-jump.spec.ts | 53 ++++++++++- tests/tool-click-context.spec.ts | 142 +++++++++++++++++++++++++++++ 11 files changed, 492 insertions(+), 8 deletions(-) create mode 100644 src/client/tool-click-context.ts create mode 100644 tests/tool-click-context.spec.ts diff --git a/src/client/EditorHost.tsx b/src/client/EditorHost.tsx index 818966ccd..17af73b12 100644 --- a/src/client/EditorHost.tsx +++ b/src/client/EditorHost.tsx @@ -492,6 +492,7 @@ export function EditorHost(props: { customData: load.customData, // The viewer's toolbar always hoists into this host's header. toolbar: 'host', + targetLine: typeof metaOf(tab).line === 'number' ? (metaOf(tab).line as number) : undefined, onToolbarState, onToolbarControls, })} diff --git a/src/client/TextEditor.tsx b/src/client/TextEditor.tsx index 459eaf064..c9fc3029c 100644 --- a/src/client/TextEditor.tsx +++ b/src/client/TextEditor.tsx @@ -60,7 +60,7 @@ const previewScrollMemory = new Map() const previewScrollKey = (scope: { sessionId: string }, path: string): string => `${scope.sessionId}::${path}` export function TextEditor(props: FileViewerProps) { - const { ctx, scope, path, viewerId, content, truncated } = props + const { ctx, scope, path, viewerId, content, truncated, targetLine } = props const [mode, setMode] = useState('preview') /** The editor's current text (null while clean); preview renders this. */ const [draft, setDraft] = useState(null) @@ -214,6 +214,20 @@ export function TextEditor(props: FileViewerProps) { }) const view = new CodeMirrorView({ state, parent: host }) viewRef.current = view + + // Scroll to target line if provided (e.g. from read tool offset) + if (typeof targetLine === 'number' && targetLine >= 1 && targetLine <= state.doc.lines) { + const lineObj = state.doc.line(targetLine) + view.dispatch({ selection: { anchor: lineObj.from } }) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const block = view.lineBlockAt(lineObj.from) + view.scrollDOM.scrollTop = Math.max(0, block.top - 8) + view.requestMeasure() + }) + }) + } + return () => { view.destroy() viewRef.current = null diff --git a/src/client/chat-preview.ts b/src/client/chat-preview.ts index 6de8a9805..7c6cb2a23 100644 --- a/src/client/chat-preview.ts +++ b/src/client/chat-preview.ts @@ -90,7 +90,7 @@ export function applyChatPreview(store: SidebarStore, tab: SidebarTab): void { if (located.where === 'float') { // Editor → editor keeps the float: patch in place and raise. if (located.tab.type === 'editor' && tab.type === 'editor') { - store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path })) + store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path, meta: tab.meta })) store.reduce(s => { const floated = floatWithTab(s, CHAT_PREVIEW_TAB_ID) return floated !== undefined ? raiseFloat(s, floated.id) : s @@ -110,7 +110,7 @@ export function applyChatPreview(store: SidebarStore, tab: SidebarTab): void { const paneId = located.paneId // Same type editor: patch in place and focus. if (located.tab.type === 'editor' && tab.type === 'editor') { - store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path })) + store.reduce(s => patchTab(s, CHAT_PREVIEW_TAB_ID, { title: tab.title, path: tab.path, meta: tab.meta })) store.reduce(s => activateTab(s, paneId, CHAT_PREVIEW_TAB_ID)) // Ensure visible: expand + pin to right (only when not floating). store.reduce(s => (s.panelOpen ? s : togglePanel(s))) diff --git a/src/client/index.tsx b/src/client/index.tsx index 9b1f67a6d..e350385a1 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -20,6 +20,7 @@ import { RenderBoundary } from './RenderBoundary.tsx' import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx' import { registerLinkInterception } from './link-intercept.ts' import { registerSubagentToolJump } from './subagent-tool-jump.ts' +import { registerToolClickTracking } from './tool-click-context.ts' import { registerImeGuard } from './ime-guard.ts' import { registerSettingsNavIcon } from './settings-nav-icon.ts' import { loadBootDecision } from './prefs.ts' @@ -411,6 +412,18 @@ export function apply(ctx: Context): void { 'dsh-better-sidebar: subagent tool jump interception', ) + ctx.effect( + () => { + try { + return registerToolClickTracking() + } catch (error) { + fail('tool click tracking', error) + return () => {} + } + }, + 'dsh-better-sidebar: tool click tracking', + ) + // The IME guard: composition keys (candidate arrows, confirm, cancel) // belong to the input method, never to page JS. Inlined third-party UI // (formerly Univer's office controls, #562 regression) has shipped diff --git a/src/client/intercept.tsx b/src/client/intercept.tsx index 8f5776157..a3765a56f 100644 --- a/src/client/intercept.tsx +++ b/src/client/intercept.tsx @@ -16,6 +16,7 @@ import { api } from './api.ts' import { buildCommitDiffTab, buildEditDiffTab, deriveEditDiffTarget } from './edit-diff.ts' import { relativeTo } from './paths.ts' import { applyChatPreview, CHAT_PREVIEW_TAB_ID } from './chat-preview.ts' +import { getLastToolContext } from './tool-click-context.ts' import css from './sidebar.module.css' /** @@ -55,12 +56,40 @@ export function openSidebarEditorFile(ctx: Context, store: SidebarStore, session * @param sessionId - owning session. * @param path - file path (relative to the session cwd or absolute). */ -export async function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): Promise { +export async function openSidebarFile( + ctx: Context, + store: SidebarStore, + sessionId: string, + path: string, + options?: { isRead?: boolean; targetLine?: number }, +): Promise { const prefs = store.getPrefs() const summary = ctx.sessions.list.getSnapshot().byId[sessionId] const cwd = summary?.cwd const absolute = resolveSidebarPath(cwd, path) let previewTab: import('./state.ts').SidebarTab | null = null + + const toolContext = getLastToolContext() + const isRead = options?.isRead ?? (toolContext?.isRead && !toolContext?.isEdit) ?? false + const targetLine = options?.targetLine ?? (isRead ? toolContext?.targetLine : undefined) + + // When opening from a read tool (or explicit read option), ONLY show the file + // in the editor (or the read content at targetLine). NEVER probe or open git diff. + if (isRead) { + const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\')) + const title = at === -1 ? absolute : absolute.slice(at + 1) + previewTab = { + id: CHAT_PREVIEW_TAB_ID, + type: 'editor', + title, + path: absolute, + ...(targetLine !== undefined ? { meta: { line: targetLine } } : {}), + } + applyChatPreview(store, previewTab) + void ctx + return + } + const canProbeDiff = prefs.editOpensDiff !== false && prefs.tabsEnabled['diff'] !== false if (canProbeDiff) { try { @@ -97,7 +126,13 @@ export async function openSidebarFile(ctx: Context, store: SidebarStore, session if (previewTab === null) { const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\')) const title = at === -1 ? absolute : absolute.slice(at + 1) - previewTab = { id: CHAT_PREVIEW_TAB_ID, type: 'editor', title, path: absolute } + previewTab = { + id: CHAT_PREVIEW_TAB_ID, + type: 'editor', + title, + path: absolute, + ...(targetLine !== undefined ? { meta: { line: targetLine } } : {}), + } } // Bypass service.openTab's dedupe (editor dedupes by path) and manipulate // the store directly so the fixed preview id always reuses the same tab. diff --git a/src/client/service.ts b/src/client/service.ts index 985b8d77f..497fc43cc 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -276,6 +276,8 @@ export interface FileViewerProps { /** Internal: the viewer registers its toolbar commands on mount (null on * unmount). */ onToolbarControls?: (controls: EditorToolbarControls | null) => void + /** Optional target line number to scroll / highlight (e.g. from read tool offset). */ + targetLine?: number } /** The toolbar state a text editor reports to the host's merged-mode header. */ diff --git a/src/client/subagent-tool-jump.ts b/src/client/subagent-tool-jump.ts index e7ce27b41..da9bcf608 100644 --- a/src/client/subagent-tool-jump.ts +++ b/src/client/subagent-tool-jump.ts @@ -14,6 +14,12 @@ import type { import { t } from './locales.ts' import { isPlainLeftClick } from './link-intercept.ts' import { firstLeaf, togglePanel, type SidebarStore } from './state.ts' +import { + findFilePathInTool, + findTargetLineInTool, + isReadTool, +} from './tool-click-context.ts' +import { openSidebarFile } from './intercept.tsx' /** Whether a tool name represents a subagent delegation or communication tool. */ export function isSubagentTool(name: string | null | undefined): boolean { @@ -157,16 +163,38 @@ export function registerSubagentToolJump( if (!target || typeof target.closest !== 'function') return // Find closest tool call container - const toolEl = target.closest('[data-tool]') + const toolEl = target.closest('[data-tool], [data-variant]') if (!toolEl) return const toolName = toolEl.getAttribute('data-tool') - if (!isSubagentTool(toolName)) return + const variant = toolEl.getAttribute('data-variant') // If user explicitly clicked a toggle chevron, let it toggle expansion const isChevron = Boolean(target.closest('[class*="chevron" i], [data-slot*="chevron" i], svg')) if (isChevron) return + // Handle clicking a read tool: open the file directly in the editor (never diff) + if (isReadTool(toolName) || isReadTool(variant)) { + const isFileLink = Boolean(target.closest('button[class*="fileLink" i]')) + if (!isFileLink) { + const filePath = findFilePathInTool(toolEl) + if (filePath) { + const snapshot = ctx.sessions?.list?.getSnapshot() + const parentSessionId = snapshot?.current + if (parentSessionId) { + event.preventDefault() + event.stopPropagation() + const targetLine = findTargetLineInTool(toolEl) + void openSidebarFile(ctx, store, parentSessionId, filePath, { isRead: true, targetLine }) + return + } + } + } + return + } + + if (!isSubagentTool(toolName)) return + const snapshot = ctx.sessions?.list?.getSnapshot() const parentSessionId = snapshot?.current if (!parentSessionId) return diff --git a/src/client/tool-click-context.ts b/src/client/tool-click-context.ts new file mode 100644 index 000000000..39e0ca049 --- /dev/null +++ b/src/client/tool-click-context.ts @@ -0,0 +1,149 @@ +/** + * Tool click context tracking: + * Tracks which tool (read / edit / write / subagent / etc.) was clicked in the chat + * so file-open funnels (`openSidebarFile`, `wrapOpenWorkspacePath`) know whether + * an open request was triggered by a read tool (show file only, never diff) + * vs an edit tool (show git diff when `editOpensDiff` is on). + */ + +export interface ActiveToolContext { + tool?: string + variant?: string + targetLine?: number + isRead: boolean + isEdit: boolean + timestamp: number +} + +let lastToolContext: ActiveToolContext | null = null + +/** Whether a tool name or variant represents a read operation. */ +export function isReadTool(nameOrVariant?: string | null): boolean { + if (!nameOrVariant) return false + const lower = nameOrVariant.trim().toLowerCase() + return ( + lower === 'read' || + lower.startsWith('read_') || + lower.startsWith('read-') || + lower === 'read_image' || + lower === 'readimage' || + lower === 'read_file' || + lower === 'readfile' || + lower.includes('read') + ) +} + +/** Whether a tool name or variant represents an edit/mutation operation. */ +export function isEditTool(nameOrVariant?: string | null): boolean { + if (!nameOrVariant) return false + const lower = nameOrVariant.trim().toLowerCase() + return ( + lower === 'edit' || + lower === 'write' || + lower.startsWith('edit_') || + lower.startsWith('write_') || + lower.startsWith('edit-') || + lower.startsWith('write-') || + lower === 'str_replace_editor' || + lower.includes('edit') || + lower.includes('write') + ) +} + +/** + * Find the file path from a tool row DOM element. + */ +export function findFilePathInTool(toolEl: Element): string | undefined { + // 1. Check fileLink button text + const fileBtn = toolEl.querySelector('button[class*="fileLink" i]') + if (fileBtn?.textContent?.trim()) return fileBtn.textContent.trim() + + // 2. Check summary span text + const summarySpan = toolEl.querySelector('span[class*="summary" i]') + if (summarySpan?.textContent?.trim()) return summarySpan.textContent.trim() + + // 3. Check SideChat meta + const metaSpan = toolEl.querySelector('[class*="RowMeta" i], [class*="sidechatRowMeta" i]') + if (metaSpan?.textContent?.trim()) return metaSpan.textContent.trim() + + // 4. Try parsing tool arguments from text or code blocks + const text = toolEl.textContent || '' + const pathMatch = text.match(/"file_path"\s*:\s*"([^"]+)"/) || text.match(/"path"\s*:\s*"([^"]+)"/) + if (pathMatch && pathMatch[1] !== undefined) return pathMatch[1] + + return undefined +} + +/** + * Extract target line number (e.g. read tool offset) from the tool row DOM element. + */ +export function findTargetLineInTool(toolEl: Element): number | undefined { + // 1. Check if toolEl has any line number indicators + const lineEl = toolEl.querySelector('[class*="number" i], [class*="lineNum" i]') + if (lineEl?.textContent) { + const num = parseInt(lineEl.textContent.trim(), 10) + if (!isNaN(num) && num > 0) return num + } + // 2. Check if toolEl contains JSON arguments with offset + const text = toolEl.textContent || '' + const offsetMatch = text.match(/"offset"\s*:\s*(\d+)/) || text.match(/\boffset\s*[:=]\s*(\d+)/i) + if (offsetMatch && offsetMatch[1] !== undefined) { + const num = parseInt(offsetMatch[1], 10) + if (!isNaN(num) && num > 0) return num + } + return undefined +} + +/** + * Capture click/pointerdown target in the DOM to record tool context. + */ +export function trackToolClick(event: Event): void { + const target = event.target as Element | null + if (!target || typeof target.closest !== 'function') return + const toolEl = target.closest('[data-tool], [data-variant]') + if (!toolEl) return + + const tool = toolEl.getAttribute('data-tool') || undefined + const variant = toolEl.getAttribute('data-variant') || undefined + const isRead = isReadTool(variant) || isReadTool(tool) + const isEdit = isEditTool(variant) || isEditTool(tool) + const targetLine = findTargetLineInTool(toolEl) + + lastToolContext = { + tool, + variant, + targetLine, + isRead, + isEdit, + timestamp: Date.now(), + } +} + +/** + * Return the most recent tool context if within the recency threshold (2 seconds). + */ +export function getLastToolContext(): ActiveToolContext | null { + if (lastToolContext !== null && Date.now() - lastToolContext.timestamp < 2000) { + return lastToolContext + } + return null +} + +/** Set tool context for testing. */ +export function setLastToolContextForTest(context: ActiveToolContext | null): void { + lastToolContext = context +} + +/** + * Register document-level click tracking. + * @returns cleanup disposer (HMR-safe) + */ +export function registerToolClickTracking(): () => void { + if (typeof document === 'undefined') return () => {} + document.addEventListener('pointerdown', trackToolClick, true) + document.addEventListener('click', trackToolClick, true) + return () => { + document.removeEventListener('pointerdown', trackToolClick, true) + document.removeEventListener('click', trackToolClick, true) + } +} diff --git a/tests/openpath-intercept.spec.ts b/tests/openpath-intercept.spec.ts index 486d3de06..29f7b5f57 100644 --- a/tests/openpath-intercept.spec.ts +++ b/tests/openpath-intercept.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' import { registerOpenPathInterception } from '../src/client/intercept.tsx' +import { setLastToolContextForTest } from '../src/client/tool-click-context.ts' import { isFolderRevealPath, wrapOpenWorkspacePath, @@ -312,4 +313,52 @@ describe('open-path interception wiring', () => { expect(previewPath()).toBe('/w/src/b.ts') expect(hostOpened).toEqual([]) }) + + it('read tool opens ONLY as an editor tab even when editOpensDiff is true', async () => { + let current = fakeNamespaceService([]) + let injectCallback: ((c: unknown) => void) | undefined + let effectDisposer: (() => void) | undefined + const ctx = { + inject: (_names: readonly string[], fn: (c: unknown) => void) => { + injectCallback = fn + fn({ + get: (name: string) => (name === 'remote.session' ? current : undefined), + effect: (eff: () => () => void) => { effectDisposer = eff() }, + }) + return { dispose: async () => { effectDisposer?.() } } + }, + sessions: { + list: { + getSnapshot: () => ({ current: 's1', byId: { s1: { id: 's1', cwd: '/w' } } }), + }, + }, + } + const store = createSidebarStore() + store.setSession('s1') + store.setPrefs({ ...store.getPrefs(), editOpensDiff: true }) + registerOpenPathInterception(ctx as unknown as Context, store) + + setLastToolContextForTest({ + tool: 'read', + isRead: true, + isEdit: false, + targetLine: 42, + timestamp: Date.now(), + }) + + await current.openWorkspacePath({ path: '/w/src/doc.ts' }) + await new Promise(resolve => setTimeout(resolve, 0)) + + const splits = store.getSnapshot().state!.splits as { + tabs: Array<{ id: string; type: string; path?: string; diff?: unknown; meta?: { line?: number } }> + } + const tab = splits.tabs.find(t => t.id === 'chat-preview') + expect(tab).toBeDefined() + expect(tab?.type).toBe('editor') + expect(tab?.path).toBe('/w/src/doc.ts') + expect(tab?.diff).toBeUndefined() + expect(tab?.meta?.line).toBe(42) + + setLastToolContextForTest(null) + }) }) diff --git a/tests/subagent-tool-jump.spec.ts b/tests/subagent-tool-jump.spec.ts index aedc83a7b..9db35d229 100644 --- a/tests/subagent-tool-jump.spec.ts +++ b/tests/subagent-tool-jump.spec.ts @@ -7,7 +7,7 @@ import { registerSubagentToolJump, } from '../src/client/subagent-tool-jump.ts' import type { Context, SidebarSessionList } from '../src/context-types.ts' -import type { SidebarStore } from '../src/client/state.ts' +import { createSidebarStore, type SidebarStore } from '../src/client/state.ts' afterEach(() => { document.body.innerHTML = '' @@ -256,4 +256,55 @@ describe('registerSubagentToolJump', () => { dispose() }) + + it('clicking a read tool row opens the file in editor', async () => { + const ctx = { + sessions: { + list: { + getSnapshot: () => ({ + current: 'p-1', + byId: { + 'p-1': { id: 'p-1', cwd: '/workspace', displayTitle: 'Parent' }, + }, + }), + }, + }, + get: vi.fn(), + } as unknown as Context + const store = createSidebarStore() + store.setSession('p-1') + store.setPrefs({ ...store.getPrefs(), editOpensDiff: true }) + + const dispose = registerSubagentToolJump(ctx, store) + + const toolEl = document.createElement('div') + toolEl.setAttribute('data-tool', 'read') + toolEl.setAttribute('data-variant', 'read') + const titleSpan = document.createElement('span') + titleSpan.className = 'toolTitle' + titleSpan.textContent = '读取' + const summarySpan = document.createElement('span') + summarySpan.className = 'summary' + summarySpan.textContent = 'src/test.ts' + toolEl.appendChild(titleSpan) + toolEl.appendChild(summarySpan) + document.body.appendChild(toolEl) + + titleSpan.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 }), + ) + + await new Promise(resolve => setTimeout(resolve, 0)) + + const splits = store.getSnapshot().state!.splits as { + tabs: Array<{ id: string; type: string; path?: string; diff?: unknown }> + } + const tab = splits.tabs.find(t => t.id === 'chat-preview') + expect(tab).toBeDefined() + expect(tab?.type).toBe('editor') + expect(tab?.path).toBe('/workspace/src/test.ts') + expect(tab?.diff).toBeUndefined() + + dispose() + }) }) diff --git a/tests/tool-click-context.spec.ts b/tests/tool-click-context.spec.ts new file mode 100644 index 000000000..176c3c04e --- /dev/null +++ b/tests/tool-click-context.spec.ts @@ -0,0 +1,142 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it } from 'vitest' +import { + isReadTool, + isEditTool, + findFilePathInTool, + findTargetLineInTool, + trackToolClick, + getLastToolContext, + setLastToolContextForTest, +} from '../src/client/tool-click-context.ts' + +afterEach(() => { + document.body.innerHTML = '' + setLastToolContextForTest(null) +}) + +describe('isReadTool', () => { + it('identifies read tool variants', () => { + expect(isReadTool('read')).toBe(true) + expect(isReadTool('read_file')).toBe(true) + expect(isReadTool('read_image')).toBe(true) + expect(isReadTool('READ')).toBe(true) + }) + + it('rejects non-read tools', () => { + expect(isReadTool('edit')).toBe(false) + expect(isReadTool('write')).toBe(false) + expect(isReadTool('bash')).toBe(false) + expect(isReadTool('subagent')).toBe(false) + expect(isReadTool(null)).toBe(false) + expect(isReadTool(undefined)).toBe(false) + }) +}) + +describe('isEditTool', () => { + it('identifies edit and write tool variants', () => { + expect(isEditTool('edit')).toBe(true) + expect(isEditTool('write')).toBe(true) + expect(isEditTool('str_replace_editor')).toBe(true) + expect(isEditTool('edit_file')).toBe(true) + }) + + it('rejects non-edit tools', () => { + expect(isEditTool('read')).toBe(false) + expect(isEditTool('bash')).toBe(false) + expect(isEditTool('subagent')).toBe(false) + expect(isEditTool(null)).toBe(false) + }) +}) + +describe('findFilePathInTool', () => { + it('finds file path from fileLink button', () => { + const el = document.createElement('div') + const btn = document.createElement('button') + btn.className = 'some_fileLink_class' + btn.textContent = 'src/client/TextEditor.tsx' + el.appendChild(btn) + + expect(findFilePathInTool(el)).toBe('src/client/TextEditor.tsx') + }) + + it('finds file path from summary span', () => { + const el = document.createElement('div') + const span = document.createElement('span') + span.className = 'some_summary_class' + span.textContent = 'src/client/DiffTab.tsx' + el.appendChild(span) + + expect(findFilePathInTool(el)).toBe('src/client/DiffTab.tsx') + }) + + it('finds file path from JSON argument text', () => { + const el = document.createElement('div') + el.textContent = '{"file_path": "src/utils/math.ts", "offset": 50}' + + expect(findFilePathInTool(el)).toBe('src/utils/math.ts') + }) +}) + +describe('findTargetLineInTool', () => { + it('extracts line number from element with number class', () => { + const el = document.createElement('div') + const numSpan = document.createElement('span') + numSpan.className = 'row_number' + numSpan.textContent = '42' + el.appendChild(numSpan) + + expect(findTargetLineInTool(el)).toBe(42) + }) + + it('extracts offset from JSON arguments in text', () => { + const el = document.createElement('div') + el.textContent = '{"file_path": "a.ts", "offset": 128}' + + expect(findTargetLineInTool(el)).toBe(128) + }) +}) + +describe('trackToolClick and getLastToolContext', () => { + it('records read tool click context accurately', () => { + const toolEl = document.createElement('div') + toolEl.setAttribute('data-tool', 'read') + toolEl.setAttribute('data-variant', 'read') + const btn = document.createElement('button') + btn.className = 'fileLink' + btn.textContent = 'src/test.ts' + toolEl.appendChild(btn) + document.body.appendChild(toolEl) + + const event = new MouseEvent('click', { bubbles: true }) + Object.defineProperty(event, 'target', { value: btn }) + + trackToolClick(event) + + const ctx = getLastToolContext() + expect(ctx).not.toBeNull() + expect(ctx?.isRead).toBe(true) + expect(ctx?.isEdit).toBe(false) + expect(ctx?.tool).toBe('read') + }) + + it('records edit tool click context accurately', () => { + const toolEl = document.createElement('div') + toolEl.setAttribute('data-tool', 'edit') + toolEl.setAttribute('data-variant', 'edit') + const btn = document.createElement('button') + toolEl.appendChild(btn) + document.body.appendChild(toolEl) + + const event = new MouseEvent('click', { bubbles: true }) + Object.defineProperty(event, 'target', { value: btn }) + + trackToolClick(event) + + const ctx = getLastToolContext() + expect(ctx).not.toBeNull() + expect(ctx?.isRead).toBe(false) + expect(ctx?.isEdit).toBe(true) + expect(ctx?.tool).toBe('edit') + }) +}) From 5e9e3dca25c310fcd45a2ded042e4bf8db1a1680 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Sat, 5 Sep 2026 21:38:25 +0800 Subject: [PATCH 10/11] fix(service): preserve external plugin tabs across sidebar reloads --- src/client/service.ts | 44 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/client/service.ts b/src/client/service.ts index 497fc43cc..7a338df74 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -509,14 +509,40 @@ function safeCall(fn: () => void): void { } } +const BUILTIN_TAB_IDS = new Set(['explorer', 'git', 'subagent', 'terminal', 'browser', 'diff', 'changes']) +const BUILTIN_VIEWER_IDS = new Set(['code', 'markdown', 'image', 'pdf', 'html', 'video']) + +const isTestEnv = typeof process !== 'undefined' && (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') + +const g = globalThis as unknown as { + __DSH_BETTER_SIDEBAR_EXTERNAL_TABS__?: Map + __DSH_BETTER_SIDEBAR_EXTERNAL_VIEWERS__?: Map +} + +function getPreservedTabs(): Map { + if (isTestEnv || typeof window === 'undefined') return new Map() + if (!g.__DSH_BETTER_SIDEBAR_EXTERNAL_TABS__) { + g.__DSH_BETTER_SIDEBAR_EXTERNAL_TABS__ = new Map() + } + return g.__DSH_BETTER_SIDEBAR_EXTERNAL_TABS__ +} + +function getPreservedViewers(): Map { + if (isTestEnv || typeof window === 'undefined') return new Map() + if (!g.__DSH_BETTER_SIDEBAR_EXTERNAL_VIEWERS__) { + g.__DSH_BETTER_SIDEBAR_EXTERNAL_VIEWERS__ = new Map() + } + return g.__DSH_BETTER_SIDEBAR_EXTERNAL_VIEWERS__ +} + /** * Create one BetterSidebar service bound to a store. The service owns the * tab/viewer registries (Map + listener set) and proxies openTab/closeTab * to the store's reducer. One instance per client plugin activation. */ export function createBetterSidebarService(store: SidebarStore): BetterSidebarService { - const tabs = new Map() - const viewers = new Map() + const tabs = new Map(getPreservedTabs()) + const viewers = new Map(getPreservedViewers()) const listeners = new Set<() => void>() const notify = (): void => { @@ -530,13 +556,20 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe const registerTab = (descriptor: TabDescriptor): (() => void) => { if (tabs.has(descriptor.id)) { + if (tabs.get(descriptor.id) === descriptor) return () => {} throw new Error(`[dsh-better-sidebar] tab type "${descriptor.id}" already registered`) } tabs.set(descriptor.id, descriptor) + if (!BUILTIN_TAB_IDS.has(descriptor.id)) { + getPreservedTabs().set(descriptor.id, descriptor) + } notify() return () => { if (tabs.get(descriptor.id) === descriptor) { tabs.delete(descriptor.id) + if (!BUILTIN_TAB_IDS.has(descriptor.id)) { + getPreservedTabs().delete(descriptor.id) + } notify() } } @@ -544,13 +577,20 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe const registerFileViewer = (descriptor: FileViewerDescriptor): (() => void) => { if (viewers.has(descriptor.id)) { + if (viewers.get(descriptor.id) === descriptor) return () => {} throw new Error(`[dsh-better-sidebar] file viewer "${descriptor.id}" already registered`) } viewers.set(descriptor.id, descriptor) + if (!BUILTIN_VIEWER_IDS.has(descriptor.id)) { + getPreservedViewers().set(descriptor.id, descriptor) + } notify() return () => { if (viewers.get(descriptor.id) === descriptor) { viewers.delete(descriptor.id) + if (!BUILTIN_VIEWER_IDS.has(descriptor.id)) { + getPreservedViewers().delete(descriptor.id) + } notify() } } From f74393b8f09334d45f1864337fdebd0981e571a4 Mon Sep 17 00:00:00 2001 From: Cinvy <274833590@qq.com> Date: Sat, 5 Sep 2026 23:55:48 +0800 Subject: [PATCH 11/11] feat(subagent): display model name next to task titles in subagent view --- src/client/SubagentView.module.css | 26 +++++ src/client/SubagentView.tsx | 71 ++++++++++++-- src/client/api.ts | 5 +- src/subagent-live-route.ts | 141 +++++++++++++++++++++++---- tests/subagent-jobs-view.spec.tsx | 26 +++++ tests/subagent-live-polling.spec.tsx | 28 ++++++ tests/subagent-live-route.spec.ts | 83 +++++++++++++++- 7 files changed, 350 insertions(+), 30 deletions(-) diff --git a/src/client/SubagentView.module.css b/src/client/SubagentView.module.css index 14ddd9b3c..0a1e0f2fd 100644 --- a/src/client/SubagentView.module.css +++ b/src/client/SubagentView.module.css @@ -133,10 +133,36 @@ } .subagentLabel { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; color: inherit; font-weight: 400; } +.subagentLabelText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.subagentModelBadge { + flex: none; + display: inline-flex; + align-items: center; + max-width: 140px; + padding: 0 5px; + border: 1px solid var(--dsw-alias-hairline); + border-radius: 999px; + font: var(--dsw-font-xxxs-11); + color: var(--dsw-alias-label-tertiary); + line-height: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .subagentSecondary { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary); diff --git a/src/client/SubagentView.tsx b/src/client/SubagentView.tsx index 10ac564e4..acc912e43 100644 --- a/src/client/SubagentView.tsx +++ b/src/client/SubagentView.tsx @@ -178,6 +178,11 @@ function SubagentLiveLines(props: { live: LastActivity | undefined }) { ) } +interface SubagentLiveData { + live: Readonly> + models: Readonly> +} + /** * One shared live-preview poller for the whole Subagent tree. Unlike the old * per-card `subagents.history` timers, this sends at most ONE `subagents.live` @@ -189,16 +194,23 @@ function SubagentLiveLines(props: { live: LastActivity | undefined }) { function useSubagentLive( rootId: string | undefined, active: boolean, -): Readonly> { +): SubagentLiveData { const [live, setLive] = useState>({}) + const [models, setModels] = useState>({}) // A new tree must never inherit another root's live previews. - useEffect(() => { setLive({}) }, [rootId]) + useEffect(() => { + setLive({}) + setModels({}) + }, [rootId]) const poll = useCallback(async (signal: AbortSignal): Promise => { if (rootId === undefined) return const result = await api.subagentsLive(rootId, signal) - if (!signal.aborted) setLive(result.live) + if (!signal.aborted) { + setLive(result.live) + if (result.models !== undefined) setModels(result.models) + } }, [rootId]) usePolling(rootId !== undefined && active, poll, { intervalMs: POLL_MS, @@ -206,7 +218,21 @@ function useSubagentLive( immediate: true, }) - return live + return { live, models } +} + +/** Resolve the model name for a session, checking the live map first then projection hints. */ +export function resolveSessionModel( + sessionId: string, + liveModels: Readonly>, + byId: Readonly>, +): string | undefined { + if (liveModels[sessionId]) return liveModels[sessionId] + const summary = byId[sessionId] + const selection = (summary as { projectionValues?: { modelSelection?: { model?: string; next?: { model?: string }; lastUsed?: { model?: string } } } })?.projectionValues?.modelSelection + const fromProjection = selection?.next?.model ?? selection?.lastUsed?.model ?? selection?.model + if (typeof fromProjection === 'string' && fromProjection !== '') return fromProjection + return undefined } interface RowsProps { @@ -219,13 +245,14 @@ interface RowsProps { currentSessionId: string /** The batch live-preview map (child id → latest activity). */ live: Readonly> + models: Readonly> openChild: (address: SidebarSubagentAddress) => void refresh: (parentSessionId: string) => void } /** Render one topology level; branches are always expanded (lazy catalogs). */ function CatalogRows({ - parentSessionId, catalog, catalogs, byId, level, currentSessionId, live, + parentSessionId, catalog, catalogs, byId, level, currentSessionId, live, models, openChild, refresh, }: RowsProps) { const emptyLoading = catalog?.state === 'loading' && catalog.entries.length === 0 @@ -281,6 +308,7 @@ function CatalogRows({ const summary = byId[entry.id] const label = childLabel(entry, summary) const secondary = cardSecondary(summary, entry) + const model = resolveSessionModel(entry.id, models, byId) const childLoading = childCatalog === undefined || (childCatalog.state === 'loading' && childCatalog.entries.length === 0) const address: SidebarSubagentAddress = { @@ -314,7 +342,14 @@ function CatalogRows({ className={css.subagentDot} /> - {label} + + {label} + {model !== undefined && ( + + {model} + + )} + {secondary} {entry.activity === 'running' && ( @@ -340,6 +375,7 @@ function CatalogRows({ level={level + 1} currentSessionId={currentSessionId} live={live} + models={models} openChild={openChild} refresh={refresh} /> @@ -462,8 +498,9 @@ function JobsSection(props: { rootId: string | undefined /** The page is visible (active tab + open panel): skip polling otherwise. */ active: boolean + models: Readonly> }) { - const { byId, jobsBySession, rootId, active } = props + const { byId, jobsBySession, rootId, active, models } = props const rows = useMemo( () => orderJobs(collectTreeJobs(byId, jobsBySession, rootId)), [byId, jobsBySession, rootId], @@ -549,6 +586,7 @@ function JobsSection(props: { const elapsed = live ? now - job.startedAt : (job.finishedAt ?? job.startedAt) - job.startedAt + const jobModel = resolveSessionModel(row.ownerSessionId, models, byId) const secondary = [ ...(multiOwner ? [row.ownerTitle] : []), jobStatusLabel(job.status, t), @@ -576,6 +614,11 @@ function JobsSection(props: { {job.kind} {job.label} + {jobModel !== undefined && ( + + {jobModel} + + )} {secondary} @@ -648,7 +691,8 @@ export function SubagentView(props: { const rootId = useMemo(() => rootAncestor(byId, sessionId), [byId, sessionId]) const rootCatalog = rootId === undefined ? undefined : catalogs[rootId] const rootSummary = rootId === undefined ? undefined : byId[rootId] - const live = useSubagentLive(rootId, active) + const { live, models } = useSubagentLive(rootId, active) + const rootModel = rootId === undefined ? undefined : resolveSessionModel(rootId, models, byId) /** Catalog owners currently consuming live membership updates. */ const observedRef = useRef(new Set()) @@ -825,7 +869,14 @@ export function SubagentView(props: { /> - {rootSummary.displayTitle !== '' ? rootSummary.displayTitle : t('subagentMainAgent')} + + {rootSummary.displayTitle !== '' ? rootSummary.displayTitle : t('subagentMainAgent')} + + {rootModel !== undefined && ( + + {rootModel} + + )} {`${t('subagentMainAgent')} · ${rootSummary.running === true ? t('subagentRunning') : t('subagentInactive')}`} @@ -847,6 +898,7 @@ export function SubagentView(props: { level={1} currentSessionId={sessionId} live={live} + models={models} openChild={openChild} refresh={refresh} /> @@ -865,6 +917,7 @@ export function SubagentView(props: { jobsBySession={list.jobsBySession} rootId={rootId} active={active} + models={models} />
diff --git a/src/client/api.ts b/src/client/api.ts index 22f259b2d..324aad1bb 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -112,7 +112,10 @@ export interface JobOutputResult { } /** The `subagents.live` response: running child id → latest activity. */ -export type SubagentLiveResult = { live: Record } +export type SubagentLiveResult = { + live: Record + models?: Record +} /** Terminal dependency status (mirror of the host's depsStatus; issue #140). */ export type TerminalDepsStatus = diff --git a/src/subagent-live-route.ts b/src/subagent-live-route.ts index f4e3d244a..803bf1e72 100644 --- a/src/subagent-live-route.ts +++ b/src/subagent-live-route.ts @@ -16,7 +16,12 @@ * - One child's events missing/corrupt → that child is skipped, the rest of * the batch still returns. */ -import type { Context, SidebarSubagentsService } from './context-types.ts' +import type { + Context, + SidebarSessionEvent, + SidebarSessionPersistenceService, + SidebarSubagentsService, +} from './context-types.ts' import { SIDE_LABEL_PREFIX } from './sidechat-core.ts' import { lastActivity, type LastActivity } from './subagent-activity.ts' import { requireString, SidebarError } from './wire.ts' @@ -24,12 +29,15 @@ import { requireString, SidebarError } from './wire.ts' /** The live-preview routes of the /sidebar JSON API. */ export interface SidebarSubagentLiveRoutes { /** - * Fold one tree's running subagent histories into a compact live map. + * Fold one tree's running subagent histories into a compact live map + * and resolve active model names for sessions in the tree. * @param payload - `{ rootSessionId }`. - * @returns `{ live: Record }`; children with - * no text/tool yet are omitted. + * @returns `{ live, models }`. */ - live(payload: unknown): Promise<{ live: Record }> + live(payload: unknown): Promise<{ + live: Record + models: Record + }> } /** @@ -40,6 +48,80 @@ export interface SidebarSubagentLiveRoutes { */ export const LIVE_WINDOW_MESSAGES = 12 +/** + * Scan session events backwards to find the active model name. + */ +export function extractModelFromEvents(events: readonly SidebarSessionEvent[]): string | undefined { + for (let i = events.length - 1; i >= 0; i--) { + const event = events[i] + if (event === undefined) continue + if (event.type === 'request/header') { + const model = (event as { data?: { header?: { config?: { model?: unknown } } } }).data?.header?.config?.model + if (typeof model === 'string' && model !== '') return model + } + if (event.type === 'request/context') { + const model = (event as { data?: { model?: unknown } }).data?.model + if (typeof model === 'string' && model !== '') return model + } + if (event.type === 'model/selection') { + const data = (event as { data?: { model?: unknown; next?: { model?: unknown }; lastUsed?: { model?: unknown } } }).data + const model = data?.model ?? data?.next?.model ?? data?.lastUsed?.model + if (typeof model === 'string' && model !== '') return model + } + if (event.type === 'subagent/descriptor') { + const model = (event as { data?: { agentModel?: unknown } }).data?.agentModel + if (typeof model === 'string' && model !== '') return model + } + } + return undefined +} + +/** Resolve the effective model name of a session from live runtime or persistence. */ +async function resolveSessionModel(ctx: Context, sessionId: string): Promise { + // 1. Live agent options + try { + const agents = ctx.get('agents') as { get(id: string): { options?: { model?: unknown } } | undefined } | undefined + const agentModel = agents?.get(sessionId)?.options?.model + if (typeof agentModel === 'string' && agentModel !== '') return agentModel + } catch { + // Live agent lookup might fail or be absent + } + + // 2. Live session requestHeader, requestContext, or snapshotEvents + try { + const session = ctx.sessions?.get(sessionId) as { + requestHeader?: () => { config?: { model?: unknown } } | undefined + requestContext?: () => { model?: unknown } | undefined + snapshotEvents?: () => readonly SidebarSessionEvent[] + } | undefined + const headerModel = session?.requestHeader?.()?.config?.model + if (typeof headerModel === 'string' && headerModel !== '') return headerModel + const contextModel = session?.requestContext?.()?.model + if (typeof contextModel === 'string' && contextModel !== '') return contextModel + const events = session?.snapshotEvents?.() + if (events !== undefined && events.length > 0) { + const eventModel = extractModelFromEvents(events) + if (eventModel !== undefined) return eventModel + } + } catch { + // In-memory session lookup might fail + } + + // 3. Cold session persistence + try { + const persistence = ctx.get('sessionPersistence') as SidebarSessionPersistenceService | undefined + if (persistence !== undefined && typeof persistence.inspect === 'function') { + const inspected = await persistence.inspect(sessionId) + const eventModel = extractModelFromEvents(inspected.events) + if (eventModel !== undefined) return eventModel + } + } catch { + // Session not found in persistence or service absent + } + + return undefined +} + /** * Build the live-preview routes bound to the plugin context. * @param ctx - host plugin context. @@ -68,26 +150,47 @@ export function buildSubagentLiveApi(ctx: Context): SidebarSubagentLiveRoutes { } const live: Record = {} + const sessionIds = new Set([rootSessionId]) + for (const entry of descendants) { - // Same gate the client renders cards on: only catalog-running - // children get live lines (spec: "仅对 running 且非 Side Chat"). - if (entry.kind !== 'child' || entry.activity !== 'running') continue + if (entry.kind !== 'child') continue // Side Chat threads ride the subagent origin but are sidebar tabs, - // never topology — keep them out of the live map too. + // never topology — keep them out of both live map and topology models. if (entry.label?.startsWith(SIDE_LABEL_PREFIX) ?? false) continue - try { - const activity = lastActivity( - ctx.sessions.get(entry.id)?.snapshotEvents() ?? [], - LIVE_WINDOW_MESSAGES, - ) - if (activity.text !== undefined || activity.tool !== undefined) { - live[entry.id] = activity + + sessionIds.add(entry.id) + // Same gate the client renders cards on: only catalog-running + // children get live lines (spec: "仅对 running 且非 Side Chat"). + if (entry.activity === 'running') { + try { + const activity = lastActivity( + ctx.sessions.get(entry.id)?.snapshotEvents() ?? [], + LIVE_WINDOW_MESSAGES, + ) + if (activity.text !== undefined || activity.tool !== undefined) { + live[entry.id] = activity + } + } catch { + // One child's event log is not readable: skip only that child. } - } catch { - // One child's event log is not readable: skip only that child. } } - return { live } + + const models: Record = {} + await Promise.all( + Array.from(sessionIds).map(async (id) => { + try { + const model = await resolveSessionModel(ctx, id) + if (model !== undefined) { + models[id] = model + } + } catch { + // Ignore error for individual session + } + }), + ) + + return { live, models } }, } } diff --git a/tests/subagent-jobs-view.spec.tsx b/tests/subagent-jobs-view.spec.tsx index 1af5b00a4..ecc5e4952 100644 --- a/tests/subagent-jobs-view.spec.tsx +++ b/tests/subagent-jobs-view.spec.tsx @@ -282,4 +282,30 @@ describe('SubagentView background jobs', () => { vi.useRealTimers() } }) + + it('renders model badge for job owners when available from live models', async () => { + vi.stubGlobal('fetch', async (url: string | URL | Request) => { + const method = String(url).split('/').pop() + if (method === 'subagents.live') { + return jsonResponse({ + ok: true, + value: { + live: {}, + models: { root: 'deepseek-v3', child: 'claude-3-opus' }, + }, + }) + } + return jsonResponse({ ok: true, value: {} }) + }) + + const store = makeStore(baseSnapshot()) + const { container, unmount } = renderRoot( + createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store) }), + ) + await act(async () => { await Promise.resolve() }) + const text = container.textContent ?? '' + expect(text).toContain('deepseek-v3') + expect(text).toContain('claude-3-opus') + unmount() + }) }) diff --git a/tests/subagent-live-polling.spec.tsx b/tests/subagent-live-polling.spec.tsx index 685c841ff..c8aa96a30 100644 --- a/tests/subagent-live-polling.spec.tsx +++ b/tests/subagent-live-polling.spec.tsx @@ -261,4 +261,32 @@ describe('SubagentView live polling', () => { expect(historySpy).not.toHaveBeenCalled() unmount() }) + + it('renders model badges after the task titles when returned in models', async () => { + vi.useFakeTimers() + const historySpy = vi.fn() + vi.stubGlobal('fetch', async (url: string | URL | Request) => { + const method = String(url).split('/').pop() + if (method === 'subagents.live') { + return jsonResponse({ + ok: true, + value: { + live: {}, + models: { root: 'deepseek-chat', a: 'claude-3-5-sonnet', b: 'gpt-4o' }, + }, + }) + } + throw new Error(`unexpected fetch ${String(url)}`) + }) + + const store = makeStore(runningSnapshot()) + const { container, unmount } = renderRoot( + createElement(SubagentView, { sessionId: 'root', active: true, ctx: makeCtx(store, historySpy) }), + ) + await act(async () => { await Promise.resolve() }) + expect(container.textContent).toContain('deepseek-chat') + expect(container.textContent).toContain('claude-3-5-sonnet') + expect(container.textContent).toContain('gpt-4o') + unmount() + }) }) diff --git a/tests/subagent-live-route.spec.ts b/tests/subagent-live-route.spec.ts index 9f8027da3..7c21eff7f 100644 --- a/tests/subagent-live-route.spec.ts +++ b/tests/subagent-live-route.spec.ts @@ -81,6 +81,7 @@ describe('subagents.live route', () => { 'running-a': { text: 'hello' }, 'running-b': { tool: { name: 'bash', args: '{"command":"ls"}' } }, }, + models: {}, }) expect(subagents.listDescendants).toHaveBeenCalledWith('root') }) @@ -91,7 +92,7 @@ describe('subagents.live route', () => { } const sessions = { get: () => session([]) } const api = buildSubagentLiveApi(ctxWith(subagents, sessions)) - await expect(api.live({ rootSessionId: 'root' })).resolves.toEqual({ live: {} }) + await expect(api.live({ rootSessionId: 'root' })).resolves.toEqual({ live: {}, models: {} }) }) it('folds only activity inside the recent 12-message window', async () => { @@ -116,6 +117,7 @@ describe('subagents.live route', () => { const api = buildSubagentLiveApi(ctxWith(subagents, sessions)) await expect(api.live({ rootSessionId: 'root' })).resolves.toEqual({ live: { windowed: { text: 'recent' } }, + models: {}, }) }) @@ -139,6 +141,7 @@ describe('subagents.live route', () => { const api = buildSubagentLiveApi(ctxWith(subagents, sessions)) await expect(api.live({ rootSessionId: 'root' })).resolves.toEqual({ live: { good: { text: 'ok' } }, + models: {}, }) }) @@ -165,4 +168,82 @@ describe('subagents.live route', () => { expect.objectContaining>({ code: 'bad-request' }), ) }) + + it('resolves model names from live agents, request headers, events, and persistence', async () => { + const subagents: SidebarSubagentsService = { + listDescendants: vi.fn(async () => [ + child('child-live-agent', { label: 'LiveAgent' }), + child('child-header', { label: 'Header' }), + child('child-events', { label: 'Events' }), + child('child-cold', { activity: 'inactive', label: 'Cold' }), + ]), + } + const agents = { + get: (id: string) => { + if (id === 'child-live-agent') { + return { options: { model: 'claude-3-5-sonnet' } } + } + return undefined + }, + } + const persistence = { + inspect: vi.fn(async (id: string) => { + if (id === 'child-cold') { + return { + meta: {}, + events: [ + { type: 'subagent/descriptor', seq: 1, time: 0, data: { agentModel: 'gpt-4o' } }, + ], + } + } + throw new Error('not found') + }), + } + const sessions = { + get: (id: string) => { + if (id === 'root') { + return { + header: { cwd: '/root' }, + requestHeader: () => ({ config: { model: 'deepseek-chat' } }), + snapshotEvents: () => [], + } + } + if (id === 'child-header') { + return { + header: { cwd: '/child-header' }, + requestHeader: () => ({ config: { model: 'deepseek-reasoner' } }), + snapshotEvents: () => [], + } + } + if (id === 'child-events') { + return { + header: { cwd: '/child-events' }, + snapshotEvents: () => [ + { type: 'model/selection', seq: 1, time: 0, data: { model: 'gemini-1.5-pro' } }, + ], + } + } + return session([]) + }, + } + const ctx = { + sessions, + get: (key: string) => { + if (key === 'subagents') return subagents + if (key === 'agents') return agents + if (key === 'sessionPersistence') return persistence + return undefined + }, + } as unknown as Context + + const api = buildSubagentLiveApi(ctx) + const result = await api.live({ rootSessionId: 'root' }) + expect(result.models).toEqual({ + root: 'deepseek-chat', + 'child-live-agent': 'claude-3-5-sonnet', + 'child-header': 'deepseek-reasoner', + 'child-events': 'gemini-1.5-pro', + 'child-cold': 'gpt-4o', + }) + }) })