dsh-rewind-plugin 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -309,7 +309,7 @@ function mutationPathOf(exec) {
309
309
  }
310
310
  function anchorSeqOf(session, cache) {
311
311
  const events = session.events;
312
- const cached = cache.get(session.id);
312
+ const cached = cache.get(session);
313
313
  if (cached !== void 0 && cached.eventsLength === events.length) return cached.anchor;
314
314
  let anchor = cached?.anchor;
315
315
  for (let i = events.length - 1; i >= (cached?.eventsLength ?? 0); i--) {
@@ -318,7 +318,7 @@ function anchorSeqOf(session, cache) {
318
318
  break;
319
319
  }
320
320
  }
321
- cache.set(session.id, { anchor, eventsLength: events.length });
321
+ cache.set(session, { anchor, eventsLength: events.length });
322
322
  return anchor;
323
323
  }
324
324
  async function resolveTarget(fs, path, cwd, signal) {
@@ -342,6 +342,8 @@ async function readTextOrUndefined(fs, target, signal) {
342
342
  }
343
343
  async function captureBefore(fs, exec, pending) {
344
344
  if (!TRACKED_TOOLS.has(exec.name)) return;
345
+ const header = exec.agent?.session.header;
346
+ if (header !== void 0 && (header.origin === "subagent" || (header.delegationDepth ?? 0) > 0)) return;
345
347
  const path = mutationPathOf(exec);
346
348
  if (path === void 0) return;
347
349
  const cwd = execSessionCwd(exec, path);
@@ -388,6 +390,7 @@ function formatPlan(plan, files) {
388
390
  } else {
389
391
  lines.push("\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u5FEB\u7167\u8BB0\u5F55\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002");
390
392
  }
393
+ lines.push(`impact=${files.length}`);
391
394
  return lines.join("\n");
392
395
  }
393
396
  function resolveOrError(events, surface, raw) {
@@ -402,56 +405,81 @@ function renderFailures(failed) {
402
405
  return `\uFF1B${failed.length} \u4E2A\u6587\u4EF6\u8FD8\u539F\u5931\u8D25\uFF1A${failed.map((f) => `${f.path}\uFF08${f.message}\uFF09`).join("\u3001")}`;
403
406
  }
404
407
  async function waitForAgentIdle(agent, signal, timeoutMs = 15e3) {
405
- const deadline = Date.now() + timeoutMs;
406
- while (agent.status !== "idle") {
407
- if (signal.aborted || Date.now() > deadline) return false;
408
- await new Promise((resolve) => setTimeout(resolve, 50));
408
+ if (signal.aborted) return false;
409
+ let timer;
410
+ let onAbort;
411
+ try {
412
+ await Promise.race([
413
+ agent.whenIdle(),
414
+ new Promise((_resolve, reject) => {
415
+ timer = setTimeout(() => reject(new Error("rewind idle wait timed out")), timeoutMs);
416
+ onAbort = () => reject(new Error("rewind idle wait aborted"));
417
+ signal.addEventListener("abort", onAbort, { once: true });
418
+ })
419
+ ]);
420
+ return true;
421
+ } catch {
422
+ return false;
423
+ } finally {
424
+ if (timer !== void 0) clearTimeout(timer);
425
+ if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
409
426
  }
410
- return true;
411
427
  }
412
- async function executeRewind(ctx, store, invocation, rawTarget, mode) {
428
+ async function executeRewind(ctx, store, invocation, rawTarget, mode, inflight) {
413
429
  const { agent } = invocation;
414
- if (agent.status !== "idle") {
415
- agent.cancel({ kind: "user" });
416
- const stopped = await waitForAgentIdle(agent, invocation.signal);
417
- if (!stopped) {
418
- return { kind: "error", text: "\u65E0\u6CD5\u505C\u6B62\u8FD0\u884C\u4E2D\u7684 agent\uFF0C\u56DE\u9000\u5DF2\u53D6\u6D88\u3002\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" };
419
- }
420
- }
421
- let plan;
422
- try {
423
- plan = resolveOrError(agent.session.events, agent.session.surface.nodes, rawTarget);
424
- } catch (error) {
425
- return rewindErrorResult(error);
430
+ const sessionId = agent.session.id;
431
+ if (inflight.has(sessionId)) {
432
+ return { kind: "error", text: "\u8BE5\u4F1A\u8BDD\u5DF2\u6709\u4E00\u4E2A\u56DE\u9000\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002" };
426
433
  }
427
- const marker = buildMarker();
428
- let event;
434
+ inflight.add(sessionId);
429
435
  try {
430
- event = agent.session.append("assistant/message", { turn: markerTurnOf(agent.session.events), step: 0, message: marker }, {
431
- surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
432
- sourceEventSeqs: [...plan.shadowedSeqs]
433
- });
434
- } catch (error) {
436
+ if (agent.status !== "idle") {
437
+ agent.cancel({ kind: "user" });
438
+ const stopped = await waitForAgentIdle(agent, invocation.signal);
439
+ if (!stopped) {
440
+ return { kind: "error", text: "\u65E0\u6CD5\u505C\u6B62\u8FD0\u884C\u4E2D\u7684 agent\uFF0C\u56DE\u9000\u5DF2\u53D6\u6D88\u3002\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" };
441
+ }
442
+ }
443
+ if (invocation.signal.aborted) {
444
+ return { kind: "error", text: "\u56DE\u9000\u5DF2\u53D6\u6D88\u3002" };
445
+ }
446
+ let plan;
447
+ try {
448
+ plan = resolveOrError(agent.session.events, agent.session.surface.nodes, rawTarget);
449
+ } catch (error) {
450
+ return rewindErrorResult(error);
451
+ }
452
+ const marker = buildMarker();
453
+ let event;
454
+ try {
455
+ event = agent.session.append("assistant/message", { turn: markerTurnOf(agent.session.events), step: 0, message: marker }, {
456
+ surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
457
+ sourceEventSeqs: [...plan.shadowedSeqs]
458
+ });
459
+ } catch (error) {
460
+ return {
461
+ kind: "error",
462
+ text: `\u56DE\u9000\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\u3002\u4F1A\u8BDD\u672A\u6539\u53D8\u3002`
463
+ };
464
+ }
465
+ let restore = "";
466
+ if (mode === "both") {
467
+ const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink(path));
468
+ const parts = [];
469
+ if (outcome.restored.length > 0) parts.push(`\u8FD8\u539F ${outcome.restored.length} \u4E2A\u6587\u4EF6`);
470
+ if (outcome.deleted.length > 0) parts.push(`\u5220\u9664 ${outcome.deleted.length} \u4E2A\u6587\u4EF6`);
471
+ if (outcome.skipped.length > 0) parts.push(`\u8DF3\u8FC7 ${outcome.skipped.length} \u4E2A\u94FE\u63A5`);
472
+ restore = parts.length > 0 ? `\uFF1B${parts.join("\u3001")}` : "\uFF1B\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u5199\u7C7B\u53D8\u66F4";
473
+ restore += renderFailures(outcome.failed);
474
+ }
435
475
  return {
436
- kind: "error",
437
- text: `\u56DE\u9000\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\u3002\u4F1A\u8BDD\u672A\u6539\u53D8\u3002`
476
+ kind: "success",
477
+ text: `\u5DF2\u64A4\u56DE seq ${plan.targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09${restore}\u3002`,
478
+ sourceEventSeq: event.seq
438
479
  };
480
+ } finally {
481
+ inflight.delete(sessionId);
439
482
  }
440
- let restore = "";
441
- if (mode === "both") {
442
- const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink(path));
443
- const parts = [];
444
- if (outcome.restored.length > 0) parts.push(`\u8FD8\u539F ${outcome.restored.length} \u4E2A\u6587\u4EF6`);
445
- if (outcome.deleted.length > 0) parts.push(`\u5220\u9664 ${outcome.deleted.length} \u4E2A\u6587\u4EF6`);
446
- if (outcome.skipped.length > 0) parts.push(`\u8DF3\u8FC7 ${outcome.skipped.length} \u4E2A\u94FE\u63A5`);
447
- restore = parts.length > 0 ? `\uFF1B${parts.join("\u3001")}` : "\uFF1B\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u5199\u7C7B\u53D8\u66F4";
448
- restore += renderFailures(outcome.failed);
449
- }
450
- return {
451
- kind: "success",
452
- text: `\u5DF2\u64A4\u56DE seq ${plan.targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09${restore}\u3002`,
453
- sourceEventSeq: event.seq
454
- };
455
483
  }
456
484
  function rewindErrorResult(error) {
457
485
  if (error instanceof RewindError) {
@@ -465,7 +493,7 @@ function rewindErrorResult(error) {
465
493
  }
466
494
  throw error;
467
495
  }
468
- async function handleRewind(ctx, store, invocation) {
496
+ async function handleRewind(ctx, store, invocation, inflight) {
469
497
  const session = invocation.agent.session;
470
498
  const input = invocation.rawInput.trim();
471
499
  if (input === "") {
@@ -473,7 +501,7 @@ async function handleRewind(ctx, store, invocation) {
473
501
  if (candidates.length === 0) {
474
502
  return { kind: "error", text: "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002" };
475
503
  }
476
- return executeRewind(ctx, store, invocation, `@${candidates[0].seq}`, "chat");
504
+ return executeRewind(ctx, store, invocation, `@${candidates[0].seq}`, "chat", inflight);
477
505
  }
478
506
  const parts = input.split(/\s+/);
479
507
  if (parts[0] === "preview") {
@@ -503,17 +531,18 @@ async function handleRewind(ctx, store, invocation) {
503
531
  /rewind ${target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6`
504
532
  };
505
533
  }
506
- return executeRewind(ctx, store, invocation, target, mode);
534
+ return executeRewind(ctx, store, invocation, target, mode, inflight);
507
535
  }
508
536
  function apply(ctx, config) {
509
537
  const store = new SnapshotStore(config?.snapshotDir);
510
538
  const pending = /* @__PURE__ */ new Map();
511
- const anchorCache = /* @__PURE__ */ new Map();
539
+ const anchorCache = /* @__PURE__ */ new WeakMap();
540
+ const inflight = /* @__PURE__ */ new Set();
512
541
  ctx.effect(function* () {
513
542
  yield ctx.commands.register({
514
543
  name: "rewind",
515
544
  description: "\u5728\u540C\u7A97\u53E3\u5185\u5C06\u5BF9\u8BDD\u56DE\u9000\u5230\u66F4\u65E9\u7684\u7528\u6237\u6D88\u606F\uFF08\u53EF\u540C\u65F6\u8FD8\u539F\u6587\u4EF6\uFF09",
516
- handler: (invocation) => handleRewind(ctx, store, invocation)
545
+ handler: (invocation) => handleRewind(ctx, store, invocation, inflight)
517
546
  });
518
547
  }, "dsh-rewind command");
519
548
  ctx.inject(["fs"], (scope) => {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @module dsh-rewind/client/hidden
7
7
  */
8
- import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-runtime/client';
8
+ import type { ChatConversationViewNode, CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
9
9
  /** Minimal chat snapshot reader the hiding logic needs. */
10
10
  export interface HiddenChat {
11
11
  readonly order: readonly string[];
@@ -15,6 +15,25 @@ export interface HiddenChat {
15
15
  }
16
16
  /** Extract the rewind target from a command outcome text ("已撤回 seq N..."). */
17
17
  export declare function targetOfOutcome(text: string | undefined): number | undefined;
18
+ /**
19
+ * True when a `/rewind` command node is an EXECUTED rewind for `seq` — the
20
+ * admission form the popover drives (`@<seq> chat` / `both`) that settled
21
+ * with a marker-carrying success outcome. The composer refill waits for
22
+ * exactly this node after the user confirms, so a history-loaded command can
23
+ * never trigger a fill.
24
+ */
25
+ export declare function isExecutedRewindCommand(node: CommandNode, seq: number): boolean;
26
+ /**
27
+ * Whether a preview outcome reports tracked file changes — the availability
28
+ * of the "rewind conversation and code" option (Claude Code hides the
29
+ * code-restore options when the checkpoint has no tracked changes).
30
+ *
31
+ * Prefers the machine-readable `impact=<n>` trailer the current host appends
32
+ * to preview text. Older host output (or a history-loaded preview row from
33
+ * before the trailer existed) has none, so it falls back to the human copy
34
+ * ("将影响 …") to keep mixed-version deployments correct.
35
+ */
36
+ export declare function hasFileImpact(text: string | undefined): boolean;
18
37
  /**
19
38
  * Anchor seqs that must be hidden from the rendered transcript so the user
20
39
  * sees the conversation as the agent sees it: every impact-preview flow node
@@ -1,23 +1,19 @@
1
1
  /**
2
- * dsh-rewind client half: the per-user-message rewind button and the
3
- * mode-selection popover, injected into the conversation DOM (pure plugin
4
- * no harness source patches).
2
+ * dsh-rewind client half: the manual `/rewind` composer guard, the locale
3
+ * registration, and the session-scoped portal bridge that renders the
4
+ * per-message rewind button (see `portals.tsx` for the button itself).
5
5
  *
6
- * Anchoring: each chat node seat renders `[data-chat-flow-kind]` with
7
- * `data-chat-anchor-key`; a `MutationObserver` tracks newly rendered user
8
- * seats and appends the rewind button into the message's IconActions row.
9
- * The seq is never parsed from DOM text the key is looked up in the runtime
10
- * snapshot (`session.getSnapshot().chat.nodes.get(key)`) to get the durable
11
- * `UserMessageNode.seq`.
12
- *
13
- * Interaction: clicking a message's button fixes the target (step one), the
14
- * popover offers the two modes (step two); "rewind conversation and code"
15
- * first fetches the impact list via `/rewind preview @seq both` and confirms
16
- * before executing. Execution always goes through `session.command(...)`, the
17
- * same host path the `/rewind` command uses.
6
+ * The button is NOT injected by hand into the DOM anymore: the plugin
7
+ * registers a bridge into the harness's `conversation.session.header.actions`
8
+ * list slot, and that bridge portals a React button into every user message's
9
+ * IconActions row the same rendering family as the copy button (a React
10
+ * child of the actions row), without touching any harness source. The
11
+ * registration is typed structurally (see `SlotsLike` in portals.tsx), so the
12
+ * plugin never imports conversation UI types and survives harness version
13
+ * drift.
18
14
  *
19
15
  * Manual composer input of `/rewind` is deliberately blocked (the guard
20
- * below): the command exists only as the per-message button's internal
16
+ * below): the command exists only as the per-message button's internal
21
17
  * channel, so any `/rewind` line typed by hand — bare or with arguments — is
22
18
  * stopped with a hint pointing at the button.
23
19
  *
@@ -27,7 +23,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
27
23
  export declare const name = "dsh-rewind";
28
24
  export declare const inject: string[];
29
25
  /**
30
- * Client plugin body: button injection + popover wiring.
31
- * @param ctx - client root context carrying `sessions` and `locale`.
26
+ * Client plugin body: composer guard + locale + the portal bridge.
27
+ * @param ctx - client root context carrying `slots`, `sessions` and `locale`.
32
28
  */
33
29
  export declare function apply(ctx: ClientContext): void;
@@ -7,6 +7,7 @@
7
7
  * @module dsh-rewind/client/popover
8
8
  */
9
9
  import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
10
+ import type { CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
10
11
  import type { RewindKey } from './locales.ts';
11
12
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
12
13
  export interface PopoverOptions {
@@ -17,9 +18,32 @@ export interface PopoverOptions {
17
18
  /** The button that opened the popover (outside-click ignore target). */
18
19
  readonly anchor: HTMLElement;
19
20
  readonly t: Translate;
21
+ /**
22
+ * Execute one rewind in the given mode. The popover closes itself first;
23
+ * the callback owns the command + composer-refill lifecycle (see
24
+ * runRewindAndFill in index.ts).
25
+ */
26
+ readonly onRewind: (mode: 'chat' | 'both') => void;
20
27
  }
21
28
  /** Close the current popover, if any. */
22
29
  export declare function closePopover(): void;
30
+ /**
31
+ * Seqs of the command nodes currently matching `match`. Sample BEFORE issuing
32
+ * a new command of the same shape so the subsequent wait can exclude them: a
33
+ * repeated preview/rewind of the same target must not settle on the previous
34
+ * command's stale outcome (e.g. an older preview that found file changes,
35
+ * after those changes were already restored).
36
+ */
37
+ export declare function knownCommandSeqs(session: SessionFace, match: (node: CommandNode) => boolean): Set<number>;
38
+ /**
39
+ * Resolve the outcome of the newest matching rewind command by watching the
40
+ * session snapshot (command/run + command/done land as one CommandNode).
41
+ * @returns the outcome text-bearing node, or null on timeout.
42
+ */
43
+ export declare function waitForCommand(session: SessionFace, match: (node: CommandNode) => boolean, timeoutMs?: number): Promise<{
44
+ kind: 'success' | 'error';
45
+ text?: string;
46
+ } | null>;
23
47
  /** Open the mode-selection popover anchored near the given button. */
24
48
  export declare function openPopover(opts: PopoverOptions): void;
25
49
  export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * dsh-rewind portal half: the per-user-message ↶ rewind button, rendered as a
3
+ * React portal inside the message's `MessageIconActions` row.
4
+ *
5
+ * Why portals (aligned with the copy button's own rendering): the copy button
6
+ * is a React child of the actions row, painted in the same commit as the
7
+ * bubble. A pure-DOM `appendChild` (the earlier approach) lands one microtask
8
+ * later and re-runs a full-transcript scan on EVERY mutation, which can push
9
+ * the paint of a newly sent bubble — the "occasional hiccup before the bubble
10
+ * shows". Portals let React own the button lifecycle (mount/unmount with the
11
+ * row, no orphaned buttons, no manual re-attach after harness re-renders),
12
+ * and the target collection is coalesced (one refresh per mutation batch) and
13
+ * diffed (no setState churn when nothing changed).
14
+ *
15
+ * Mount point: the plugin registers a session-scoped bridge into the harness's
16
+ * `conversation.session.header.actions` list slot. The bridge renders NO
17
+ * header UI — it only portals buttons into the user rows of the session the
18
+ * harness mounts it for. That slot is the harness-native way to get a
19
+ * per-session React mount without touching any source; the registration is
20
+ * typed structurally (see `SlotsLike`) so the plugin never imports the
21
+ * conversation UI package's types and survives its version drift.
22
+ *
23
+ * @module dsh-rewind/client/portals
24
+ */
25
+ import { type ReactNode } from 'react';
26
+ import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
27
+ import type { RewindKey } from './locales.ts';
28
+ type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
29
+ /** Capabilities the session-scoped bridge receives from the plugin apply(). */
30
+ export interface RewindBridgeDeps {
31
+ readonly sessionOf: (sessionId: string) => SessionFace | undefined;
32
+ readonly currentSessionId: () => string | undefined;
33
+ readonly t: Translate;
34
+ readonly subscribeLocale: (cb: () => void) => () => void;
35
+ }
36
+ /** Structural face of the runtime slot service (see the module doc). */
37
+ export interface SlotsLike {
38
+ inject(key: string, install: () => () => void): () => void;
39
+ register(entry: {
40
+ readonly name: string;
41
+ readonly id: string;
42
+ readonly order: number;
43
+ }, component: (props: {
44
+ readonly sessionId: string;
45
+ }) => ReactNode): () => void;
46
+ }
47
+ interface RewindPortalsProps extends RewindBridgeDeps {
48
+ readonly sessionId: string;
49
+ }
50
+ /**
51
+ * Session-scoped portal bridge: renders the ↶ button of every user message
52
+ * row of the session the harness mounts it for. The refresh is coalesced
53
+ * (one pass per mutation batch via queueMicrotask) and diffed (setState is
54
+ * skipped when the target set is unchanged), so the plugin never runs a
55
+ * synchronous full-transcript scan inside a commit microtask.
56
+ */
57
+ export declare function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
58
+ /**
59
+ * Build the slot-entry component for the plugin apply(): a tiny bridge that
60
+ * injects the apply-time capabilities (session resolution, locale, rewind
61
+ * runner) into the module-level `RewindPortals`.
62
+ */
63
+ export declare function createRewindBridge(deps: RewindBridgeDeps): (props: {
64
+ readonly sessionId: string;
65
+ }) => ReactNode;
66
+ export {};
@@ -5,8 +5,6 @@
5
5
  *
6
6
  * @module dsh-rewind/client/styles
7
7
  */
8
- /** Marker attribute set on a seat row once its rewind button is attached. */
9
- export declare const REWIND_ATTACHED = "data-dsh-rewind-attached";
10
8
  /** Class names shared between the injected DOM and the stylesheet. */
11
9
  export declare const CLASS: {
12
10
  readonly button: 'dsh-rewind-btn';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "DeepSeek Harness plugin: in-place conversation rewind in the same session window (Claude Code /rewind semantics) with optional workspace file restore",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -54,7 +54,8 @@
54
54
  "client": {
55
55
  "inject": [
56
56
  "@deepseek-ai/dsh-client-locale",
57
- "@deepseek-ai/dsh-client-runtime"
57
+ "@deepseek-ai/dsh-client-runtime",
58
+ "@deepseek-ai/dsh-client-ui-conversation"
58
59
  ],
59
60
  "platform": "web"
60
61
  }
@@ -126,7 +127,11 @@
126
127
  "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
127
128
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
128
129
  "@types/node": "^24.0.0",
130
+ "@types/react": "^18.3.31",
131
+ "@types/react-dom": "^18.3.7",
129
132
  "esbuild": "^0.28.2",
133
+ "react": "^18.3.1",
134
+ "react-dom": "^18.3.1",
130
135
  "typescript": "^7.0.2",
131
136
  "vitest": "^4.1.10"
132
137
  }