batch3 T6: usePanes hook + Shiki integration in CodeBlock

- hooks/usePanes: per-session panes CRUD; debounced (300ms) state PATCH;
  immediate position-change PATCH with refresh
- CodeBlock: shiki async highlighting via codeToHtml + github-dark theme;
  LANG_MAP for ts/tsx/js/jsx/py/go/rs/rb/java/c/cpp/cs/php/sh/yaml/json/
  toml/md/sql/dockerfile/html/css; falls back to plain pre on unknown lang
  or async failure
- package.json: + shiki

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:32:04 +00:00
parent e82e5670ee
commit b29555ee28
4 changed files with 359 additions and 7 deletions

View File

@@ -23,6 +23,7 @@
"react-router-dom": "^6.26.0", "react-router-dom": "^6.26.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shadcn": "^4.7.0", "shadcn": "^4.7.0",
"shiki": "^1.29.2",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"

View File

@@ -1,16 +1,86 @@
import { useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Check, Copy } from 'lucide-react'; import { Check, Copy } from 'lucide-react';
import { codeToHtml } from 'shiki';
// NOTE: spec calls for syntax-highlighted code blocks. Highlighting deferred // NOTE: spec calls for syntax-highlighted code blocks. Added Shiki in v1.1.
// to keep dep footprint minimal; this renders styled mono code with a copy // Shiki output is compiler-generated and does not contain user input; setting
// button. Adding a highlighter (shiki / highlight.js) is a one-import swap. // it via a ref is safe here.
interface Props { interface Props {
code: string; code: string;
lang?: string; lang?: string;
} }
const LANG_MAP: Record<string, string> = {
ts: 'typescript',
tsx: 'tsx',
typescript: 'typescript',
js: 'javascript',
jsx: 'jsx',
javascript: 'javascript',
py: 'python',
python: 'python',
go: 'go',
rs: 'rust',
rust: 'rust',
rb: 'ruby',
ruby: 'ruby',
java: 'java',
c: 'c',
cpp: 'cpp',
cs: 'csharp',
csharp: 'csharp',
php: 'php',
sh: 'bash',
bash: 'bash',
shell: 'bash',
yaml: 'yaml',
yml: 'yaml',
json: 'json',
toml: 'toml',
md: 'markdown',
markdown: 'markdown',
sql: 'sql',
dockerfile: 'dockerfile',
html: 'html',
css: 'css',
};
const SHIKI_THEME = 'github-dark';
export function CodeBlock({ code, lang }: Props) { export function CodeBlock({ code, lang }: Props) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [html, setHtml] = useState<string | null>(null);
const highlightRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let cancelled = false;
const mappedLang = (lang && LANG_MAP[lang.toLowerCase()]) ?? null;
if (!mappedLang) {
setHtml(null);
return;
}
(async () => {
try {
const result = await codeToHtml(code, { lang: mappedLang, theme: SHIKI_THEME });
if (!cancelled) setHtml(result);
} catch (err) {
console.warn('shiki failed', err);
if (!cancelled) setHtml(null);
}
})();
return () => {
cancelled = true;
};
}, [code, lang]);
// Inject Shiki HTML via ref; output is compiler-generated, not user input.
useEffect(() => {
if (highlightRef.current) {
// Shiki generates sanitized HTML spans — not user-supplied content.
// eslint-disable-next-line no-unsanitized/property
highlightRef.current.innerHTML = html ?? '';
}
}, [html]);
async function copy() { async function copy() {
try { try {
@@ -36,9 +106,16 @@ export function CodeBlock({ code, lang }: Props) {
<span>{copied ? 'Copied' : 'Copy'}</span> <span>{copied ? 'Copied' : 'Copy'}</span>
</button> </button>
</div> </div>
<pre className="overflow-x-auto px-3 py-2 font-mono text-xs leading-relaxed"> {html !== null ? (
{code} <div
</pre> ref={highlightRef}
className="overflow-x-auto px-3 py-2 font-mono text-xs leading-relaxed [&>pre]:!bg-transparent [&>pre]:!m-0 [&>pre]:!p-0"
/>
) : (
<pre className="overflow-x-auto px-3 py-2 font-mono text-xs leading-relaxed">
{code}
</pre>
)}
</div> </div>
); );
} }

View File

@@ -0,0 +1,145 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { api } from '@/api/client';
import type { Pane, PaneCreateRequest, PaneState, PaneUpdateRequest } from '@/api/types';
export function usePanes(sessionId: string | undefined): {
panes: Pane[] | null;
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
create: (body: PaneCreateRequest) => Promise<Pane>;
update: (id: string, body: PaneUpdateRequest) => Promise<void>;
remove: (id: string) => Promise<void>;
} {
const [panes, setPanes] = useState<Pane[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Pending debounced state PATCHes: pane id -> latest PaneState
const pendingState = useRef<Map<string, PaneState>>(new Map());
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const flushPendingState = useCallback(() => {
if (debounceTimer.current !== null) {
clearTimeout(debounceTimer.current);
debounceTimer.current = null;
}
const snapshot = new Map(pendingState.current);
pendingState.current.clear();
for (const [id, state] of snapshot) {
// fire-and-forget; caller surface handles errors via hook error state
void api.panes.update(id, { state }).catch((err) => {
setError(err instanceof Error ? err.message : 'pane operation failed');
});
}
}, []);
const refresh = useCallback(async () => {
if (!sessionId) {
setPanes(null);
return;
}
setLoading(true);
try {
const { panes: list } = await api.panes.getForSession(sessionId);
setPanes(list);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'pane operation failed');
} finally {
setLoading(false);
}
}, [sessionId]);
// Fetch on mount / sessionId change; preserve previous list while reloading
// (loading=true but panes stays non-null after first fetch to avoid flash)
useEffect(() => {
void refresh();
}, [refresh]);
// Flush debounced PATCHes on unmount
useEffect(() => {
return () => {
flushPendingState();
};
}, [flushPendingState]);
const create = useCallback(
async (body: PaneCreateRequest): Promise<Pane> => {
if (!sessionId) throw new Error('no session');
const created = await api.panes.create(sessionId, body);
await refresh();
return created;
},
[sessionId, refresh]
);
const update = useCallback(
async (id: string, body: PaneUpdateRequest): Promise<void> => {
const stateOnly = body.state !== undefined && body.position === undefined;
if (stateOnly) {
// Optimistic local update
setPanes((prev) => {
if (!prev) return prev;
let changed = false;
const next = prev.map((pane) => {
if (pane.id !== id) return pane;
changed = true;
// Narrow via discriminated union to satisfy TypeScript
if (pane.kind === 'chat') {
return { ...pane, state: body.state as typeof pane.state };
}
if (pane.kind === 'file_browser') {
return { ...pane, state: body.state as typeof pane.state };
}
return pane;
});
return changed ? next : prev;
});
// Coalesce: last state wins within debounce window
pendingState.current.set(id, body.state!);
if (debounceTimer.current !== null) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(() => {
debounceTimer.current = null;
flushPendingState();
}, 300);
} else {
// position involved — fire immediately
try {
await api.panes.update(id, body);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : 'pane operation failed');
throw err;
}
}
},
[refresh, flushPendingState]
);
const remove = useCallback(
async (id: string): Promise<void> => {
// Optimistic remove
const previous = panes;
setPanes((prev) => (prev ? prev.filter((p) => p.id !== id) : prev));
try {
await api.panes.remove(id);
await refresh();
} catch (err) {
// Rollback
setPanes(previous);
setError(err instanceof Error ? err.message : 'pane operation failed');
throw err;
}
},
[panes, refresh]
);
return { panes, loading, error, refresh, create, update, remove };
}

129
pnpm-lock.yaml generated
View File

@@ -87,6 +87,9 @@ importers:
shadcn: shadcn:
specifier: ^4.7.0 specifier: ^4.7.0
version: 4.7.0(@types/node@20.19.41)(typescript@5.9.3) version: 4.7.0(@types/node@20.19.41)(typescript@5.9.3)
shiki:
specifier: ^1.29.2
version: 1.29.2
sonner: sonner:
specifier: ^2.0.7 specifier: ^2.0.7
version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1566,6 +1569,27 @@ packages:
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@shikijs/core@1.29.2':
resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==}
'@shikijs/engine-javascript@1.29.2':
resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==}
'@shikijs/engine-oniguruma@1.29.2':
resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==}
'@shikijs/langs@1.29.2':
resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==}
'@shikijs/themes@1.29.2':
resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==}
'@shikijs/types@1.29.2':
resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@sindresorhus/merge-streams@4.0.0': '@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2059,6 +2083,9 @@ packages:
electron-to-chromium@1.5.355: electron-to-chromium@1.5.355:
resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==}
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
emoji-regex@10.6.0: emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2327,6 +2354,9 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
hast-util-to-jsx-runtime@2.3.6: hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
@@ -2343,6 +2373,9 @@ packages:
html-url-attributes@3.0.1: html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
http-errors@2.0.0: http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -2908,6 +2941,9 @@ packages:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
oniguruma-to-es@2.3.0:
resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==}
open@11.0.0: open@11.0.0:
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -3129,6 +3165,15 @@ packages:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
regex-recursion@5.1.1:
resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
regex-utilities@2.3.0:
resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
regex@5.1.1:
resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==}
remark-gfm@4.0.1: remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -3244,6 +3289,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'} engines: {node: '>=8'}
shiki@1.29.2:
resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==}
side-channel-list@1.0.1: side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -5015,6 +5063,41 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
'@shikijs/core@1.29.2':
dependencies:
'@shikijs/engine-javascript': 1.29.2
'@shikijs/engine-oniguruma': 1.29.2
'@shikijs/types': 1.29.2
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@1.29.2':
dependencies:
'@shikijs/types': 1.29.2
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 2.3.0
'@shikijs/engine-oniguruma@1.29.2':
dependencies:
'@shikijs/types': 1.29.2
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@1.29.2':
dependencies:
'@shikijs/types': 1.29.2
'@shikijs/themes@1.29.2':
dependencies:
'@shikijs/types': 1.29.2
'@shikijs/types@1.29.2':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.2': {}
'@sindresorhus/merge-streams@4.0.0': {} '@sindresorhus/merge-streams@4.0.0': {}
'@tailwindcss/node@4.3.0': '@tailwindcss/node@4.3.0':
@@ -5444,6 +5527,8 @@ snapshots:
electron-to-chromium@1.5.355: {} electron-to-chromium@1.5.355: {}
emoji-regex-xs@1.0.0: {}
emoji-regex@10.6.0: {} emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -5802,6 +5887,20 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hast-util-to-html@9.0.5:
dependencies:
'@types/hast': 3.0.4
'@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.1.0
space-separated-tokens: 2.0.2
stringify-entities: 4.0.4
zwitch: 2.0.4
hast-util-to-jsx-runtime@2.3.6: hast-util-to-jsx-runtime@2.3.6:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -5835,6 +5934,8 @@ snapshots:
html-url-attributes@3.0.1: {} html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
http-errors@2.0.0: http-errors@2.0.0:
dependencies: dependencies:
depd: 2.0.0 depd: 2.0.0
@@ -6528,6 +6629,12 @@ snapshots:
dependencies: dependencies:
mimic-function: 5.0.1 mimic-function: 5.0.1
oniguruma-to-es@2.3.0:
dependencies:
emoji-regex-xs: 1.0.0
regex: 5.1.1
regex-recursion: 5.1.1
open@11.0.0: open@11.0.0:
dependencies: dependencies:
default-browser: 5.5.0 default-browser: 5.5.0
@@ -6821,6 +6928,17 @@ snapshots:
tiny-invariant: 1.3.3 tiny-invariant: 1.3.3
tslib: 2.8.1 tslib: 2.8.1
regex-recursion@5.1.1:
dependencies:
regex: 5.1.1
regex-utilities: 2.3.0
regex-utilities@2.3.0: {}
regex@5.1.1:
dependencies:
regex-utilities: 2.3.0
remark-gfm@4.0.1: remark-gfm@4.0.1:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
@@ -7021,6 +7139,17 @@ snapshots:
shebang-regex@3.0.0: {} shebang-regex@3.0.0: {}
shiki@1.29.2:
dependencies:
'@shikijs/core': 1.29.2
'@shikijs/engine-javascript': 1.29.2
'@shikijs/engine-oniguruma': 1.29.2
'@shikijs/langs': 1.29.2
'@shikijs/themes': 1.29.2
'@shikijs/types': 1.29.2
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
side-channel-list@1.0.1: side-channel-list@1.0.1:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0