({ ...prev, model: next }));
}}
>
+
Choose a model after testing connection…
{options.map((id) => (
{id}
@@ -638,11 +648,12 @@ export function SettingsPanel(props: SettingsPanelProps) {
))}
Custom…
- {customModel || !options.includes(provider.model) ? (
+ {customModel || (Boolean(provider.model.trim()) && !options.includes(provider.model)) ? (
onProviderChange((prev) => ({
...prev,
@@ -741,16 +752,7 @@ export function SettingsPanel(props: SettingsPanelProps) {
onDraftChange={setDraftMaxOutput}
onCommit={(value) => {
setDraftMaxOutput(undefined);
- // Keep auto (0) when the user accepts the derived reserve for
- // the current window; only store a hard override when they
- // pick a different number (or explicitly type 0).
- const next =
- value === 0
- ? 0
- : autoMaxOutput &&
- value === livePreview.maximumOutputTokens
- ? 0
- : normalizeMaximumOutputTokens(value);
+ const next = normalizeMaximumOutputTokens(value);
onProviderChange((prev) => ({
...prev,
maximumOutputTokens: next,
@@ -765,10 +767,12 @@ export function SettingsPanel(props: SettingsPanelProps) {
? ` (currently ${provider.effectiveContextWindow.toLocaleString()} tokens)`
: ''
}.`
- : `Context window will save as ${provider.contextWindow.toLocaleString()} tokens.`}{' '}
+ : `Context window will save as ${provider.contextWindow.toLocaleString()} tokens.`}
+
+
{autoMaxOutput
- ? `Max output follows the window (~${livePreview.maximumOutputTokens.toLocaleString()} tokens). Set a different number to hard-override.`
- : `Max output will save as ${storedMaxOutput.toLocaleString()} tokens.`}
+ ? `Max output auto-scales with the window (${livePreview.maximumOutputTokens.toLocaleString()} tokens). Enter 0 to keep auto, or a positive number to override.`
+ : `Max output will save as ${previewMaxOutput.toLocaleString()} tokens (fixed override). Set to 0 to resume auto-scaling.`}
{ui.tokenBudget.enabled ? (
@@ -1128,7 +1132,11 @@ export function SettingsPanel(props: SettingsPanelProps) {
/>
) : null}
+ {activeTab === 'features' ? (
+
+
+
+
+ onSaveUi({
+ features: { codeReviewButton: e.target.checked },
+ })
+ }
+ />
+ Show Code Review button
+
+
+ Review always lists staged/unstaged files.
+ Enable this to also show Code Review , which
+ runs the code-review skill over those changes. Off by default.
+
+
+
+ ) : null}
+
{activeTab === 'integrations' ? (
void;
- onOpenFile: (path: string) => void;
- onOpenDiff: (path: string) => void;
- onOpenFinding?: (path: string, line?: number) => void;
- onRunReview: () => void;
- onUndoAll?: () => void;
- onKeepAll?: () => void;
- /** Clear sticky review findings (Problems + comments + chips). */
- onDismissFindings?: () => void;
- /** Fix one finding by index into `findings`. */
- onFixFinding?: (index: number) => void;
- /** Fix every open finding via the host recipe. */
- onFixAllFindings?: () => void;
-}
-
-function statusLabel(status: string): string {
- const normalized = status.trim() || '?';
- if (normalized.includes('A') || normalized === 'A') return 'Added';
- if (normalized.includes('D')) return 'Deleted';
- if (normalized.includes('R')) return 'Renamed';
- if (normalized.includes('?')) return 'Untracked';
- if (normalized.includes('M') || normalized === 'M') return 'Edited';
- return normalized;
-}
-
-function statusTone(status: string): string {
- const normalized = status.trim();
- if (normalized.includes('A') || normalized.includes('?') || normalized === 'A') {
- return 'added';
- }
- if (normalized.includes('D')) return 'deleted';
- return 'edited';
-}
-
-function severityTone(severity: string): string {
- const s = severity.toLowerCase();
- if (s === 'critical' || s === 'high') return 'high';
- if (s === 'medium') return 'medium';
- return 'low';
-}
-
/**
- * Flush review strip attached to the top of the composer box (Cursor-style).
- * Review is an action — not a chat mode.
+ * Re-export for existing App imports. Implementation lives in `../review/`.
*/
-export function WorkingTreeReviewBar({
- review,
- findings = [],
- runChanges = null,
- running = false,
- expandSignal = 0,
- onRefresh,
- onOpenFile,
- onOpenDiff,
- onOpenFinding,
- onRunReview,
- onUndoAll,
- onKeepAll,
- onDismissFindings,
- onFixFinding,
- onFixAllFindings,
-}: WorkingTreeReviewBarProps) {
- const files = review?.files ?? [];
- const fileCount = files.length > 0 ? files.length : (runChanges?.files.length ?? 0);
- const [expanded, setExpanded] = useState(false);
- const [tab, setTab] = useState<'files' | 'findings'>('files');
-
- const openFindings = findings.filter((f) => f.status !== 'fixed');
- const fixedFindings = findings.filter((f) => f.status === 'fixed');
- const reviewComplete =
- findings.length > 0 && openFindings.length === 0 && fixedFindings.length > 0;
- const hasOpenFindings = openFindings.length > 0;
-
- useEffect(() => {
- if (expandSignal > 0) setExpanded(true);
- }, [expandSignal]);
-
- useEffect(() => {
- if (findings.length > 0) {
- setTab('findings');
- setExpanded(true);
- }
- }, [findings.length]);
-
- useEffect(() => {
- if (reviewComplete) setExpanded(false);
- }, [reviewComplete]);
-
- if (fileCount === 0 && findings.length === 0 && !running) {
- return null;
- }
-
- const canUndoKeep =
- !reviewComplete && Boolean(runChanges && runChanges.files.length > 0);
- const canFix =
- hasOpenFindings && !running && Boolean(onFixAllFindings) && !reviewComplete;
- const listFiles =
- files.length > 0
- ? files.map((f) => ({ path: f.path, status: f.status }))
- : (runChanges?.files ?? []).map((f) => ({
- path: f.path,
- status: f.status,
- }));
-
- return (
-
-
-
setExpanded((v) => !v)}
- >
-
- ›
-
-
- {fileCount} File{fileCount === 1 ? '' : 's'}
-
- {reviewComplete ? (
-
- ✓
-
- ) : null}
- {hasOpenFindings ? (
-
- {openFindings.length} open
-
- ) : null}
- {reviewComplete ? (
- Done
- ) : fixedFindings.length > 0 ? (
-
- {fixedFindings.length} fixed
-
- ) : null}
-
- {!reviewComplete ? (
-
- onUndoAll?.()}
- >
- Undo All
-
- {
- onKeepAll?.();
- setExpanded(false);
- }}
- >
- Keep All
-
- {findings.length > 0 && onDismissFindings ? (
- {
- onDismissFindings();
- setExpanded(false);
- }}
- >
- Dismiss
-
- ) : null}
- {canFix ? (
- onFixAllFindings?.()}
- >
- Fix all
-
- ) : null}
-
- {running ? 'Reviewing…' : 'Review'}
-
-
- ) : (
-
- {onDismissFindings ? (
- {
- onDismissFindings();
- setExpanded(false);
- }}
- >
- Clear
-
- ) : null}
-
- )}
-
- {expanded ? (
-
-
-
- setTab('files')}
- >
- Files
-
- setTab('findings')}
- >
- Findings{findings.length ? ` (${findings.length})` : ''}
-
-
-
- Refresh
-
-
- {tab === 'findings' ? (
- findings.length === 0 ? (
-
- No findings yet. Click Review — issues appear on the code line
- in the editor (and in Problems).
-
- ) : (
-
- {findings.map((f, i) => {
- const fixed = f.status === 'fixed';
- return (
-
- {
- if (onOpenFinding) onOpenFinding(f.path, f.startLine);
- else onOpenFile(f.path);
- }}
- title={`Open ${f.path}${f.startLine ? `:${f.startLine}` : ''}`}
- >
-
- {fixed ? 'fixed' : f.severity}
-
-
- {f.path}
- {f.startLine ? `:${f.startLine}` : ''}
-
- {f.content}
-
- {!fixed && onFixFinding ? (
- onFixFinding(i)}
- >
- Fix
-
- ) : fixed ? (
-
- ✓
-
- ) : null}
-
- );
- })}
-
- )
- ) : listFiles.length === 0 ? (
-
No changed files.
- ) : (
-
- {listFiles.map((file) => {
- const deleted = String(file.status).includes('D');
- return (
-
- onOpenDiff(file.path)}
- title={`Open diff for ${file.path}`}
- >
-
- {statusLabel(String(file.status))}
-
- {file.path}
-
- onOpenFile(file.path)}
- >
- Open
-
-
- );
- })}
-
- )}
-
- ) : null}
-
- );
-}
+export {
+ WorkingTreeReviewBar,
+ type ReviewFindingChip,
+} from '../review/WorkingTreeReviewBar';
diff --git a/apps/vscode/webview-ui/src/protocol.ts b/apps/vscode/webview-ui/src/protocol.ts
index 58bae2d4..2f46110c 100644
--- a/apps/vscode/webview-ui/src/protocol.ts
+++ b/apps/vscode/webview-ui/src/protocol.ts
@@ -16,6 +16,7 @@ export type SettingsTab =
| 'modes'
| 'context'
| 'integrations'
+ | 'features'
| 'debug';
export type McpTransport = 'stdio' | 'sse' | 'streamable-http';
@@ -218,6 +219,8 @@ export type SemanticIndexSource =
| 'disabled';
export interface IndexStatusSnapshot {
+ discoveredFileCount?: number;
+ progressStage?: string;
fileCount: number;
truncated: boolean;
maximumIndexFiles?: number;
@@ -248,6 +251,15 @@ export interface WorkspaceSnapshotInfo {
displayRoot?: string;
}
+/** Opt-in VS Code UI features (Settings → Features). */
+export interface UiFeaturesSnapshot {
+ /**
+ * When true, show a Code Review button beside Review on the working-tree bar.
+ * Default off — Review alone covers quick git-change review.
+ */
+ codeReviewButton: boolean;
+}
+
export interface UiSettingsSnapshot {
showReasoning: boolean;
reasoningPreviewMaxChars: number;
@@ -273,6 +285,8 @@ export interface UiSettingsSnapshot {
* key is a boolean leaf in VS Code settings.)
*/
modelIoLogging: boolean;
+ /** Opt-in IDE features (VS Code only). */
+ features: UiFeaturesSnapshot;
/** Window-proportional token budget tunables (Debug → developer). */
tokenBudget: TokenBudgetSettingsSnapshot;
/** Agent Engine loop/stall threshold tunables (Debug → developer). */
@@ -419,12 +433,14 @@ export type UiSettingsPatch = Partial<
| 'tokenBudget'
| 'loopPolicy'
| 'policyLab'
+ | 'features'
> & {
contextToggles?: Partial
;
runBudget?: Partial;
modeDefaults?: Partial<
Record<'ask' | 'plan' | 'agent', Partial>
>;
+ features?: Partial;
tokenBudget?: {
enabled?: boolean;
policy?: Record;
@@ -684,6 +700,11 @@ export type WebviewToHostMessage =
requiredSkillIds?: string[];
/** Attached MCP server ids for this turn (@mcp: / pin chips). */
requiredMcpServerIds?: string[];
+ /**
+ * When mode is review: `changes` = quick diff bug scan (default);
+ * `code` = thorough code-review-and-quality pass.
+ */
+ reviewKind?: 'changes' | 'code';
}
| { type: 'cancel' }
| {
@@ -891,7 +912,7 @@ export type HostToWebviewMessage =
| { type: 'paths.results'; requestId: string; suggestions: PathSuggestion[] }
| { type: 'openSettings'; tab?: SettingsTab }
| { type: 'setTab'; tab: UiNav }
- /** Open chat in Review mode; optionally auto-start an LLM review. */
+ /** Run a working-tree review via the Review button (not a chat mode). */
| { type: 'startReview'; autoRun?: boolean; prompt?: string }
| { type: 'editorPin'; path: string; source?: ContextPinSource }
| { type: 'editorUnpin'; path: string }
diff --git a/apps/vscode/webview-ui/src/review/ComposerReviewStrip.tsx b/apps/vscode/webview-ui/src/review/ComposerReviewStrip.tsx
new file mode 100644
index 00000000..26bd41b3
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/ComposerReviewStrip.tsx
@@ -0,0 +1,83 @@
+import type { ReviewDiffView, RunFileChangesView } from '../protocol';
+import { WorkingTreeReviewBar } from './WorkingTreeReviewBar';
+import type { ReviewFindingChip } from './reviewFindingTypes';
+import {
+ composerNeedsReviewStrip,
+ selectLatestRunChanges,
+} from './selectLatestRunChanges';
+
+type TurnLike = { fileChanges?: RunFileChangesView };
+
+type Props = {
+ review: ReviewDiffView | null;
+ findings: readonly ReviewFindingChip[];
+ turns: readonly TurnLike[];
+ running: boolean;
+ expandSignal: number;
+ codeReviewEnabled: boolean;
+ onRefresh: () => void;
+ onOpenFile: (path: string) => void;
+ onOpenDiff: (path: string) => void;
+ onOpenFinding: (path: string, line?: number) => void;
+ onShowChanges: () => void;
+ onRunCodeReview: () => void;
+ onUndoFileChanges: (runId: string) => void;
+ onDismissFileChanges: (runId: string) => void;
+ onDismissFindings: () => void;
+ onFixAllFindings: () => void;
+ onFixFinding: (index: number) => void;
+};
+
+/**
+ * Composer-attached review strip with dual-scope wiring.
+ * Keeps App.tsx free of run-change lookup / feature-gate details.
+ */
+export function ComposerReviewStrip({
+ review,
+ findings,
+ turns,
+ running,
+ expandSignal,
+ codeReviewEnabled,
+ onRefresh,
+ onOpenFile,
+ onOpenDiff,
+ onOpenFinding,
+ onShowChanges,
+ onRunCodeReview,
+ onUndoFileChanges,
+ onDismissFileChanges,
+ onDismissFindings,
+ onFixAllFindings,
+ onFixFinding,
+}: Props) {
+ const runChanges = selectLatestRunChanges(turns);
+
+ return (
+ {
+ if (runChanges) onUndoFileChanges(runChanges.runId);
+ }}
+ onKeepAll={() => {
+ if (runChanges) onDismissFileChanges(runChanges.runId);
+ }}
+ onDismissFindings={onDismissFindings}
+ onFixAllFindings={onFixAllFindings}
+ onFixFinding={onFixFinding}
+ />
+ );
+}
+
+export { composerNeedsReviewStrip, selectLatestRunChanges };
diff --git a/apps/vscode/webview-ui/src/review/README.md b/apps/vscode/webview-ui/src/review/README.md
new file mode 100644
index 00000000..16ca8b3f
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/README.md
@@ -0,0 +1,41 @@
+# Review bar (webview)
+
+Composer strip above the Mitii chat input. Host ownership stays in `apps/vscode/src/`
+(`reviewDiff.ts`, `review/reviewFindingsPresenter.ts`); this folder is UI + scope rules only.
+
+## Dual-scope contract
+
+| Action / surface | Data source | LLM |
+|---|---|---|
+| Summary (`2 file changes`) | Latest run `RunFileChangesView` | No |
+| **Review** + Files tab | Same chat/run edits | No |
+| **Code Review (N)** | Full git `ReviewDiffView` (`N` = file count) | Yes |
+| Findings tab, severity chips, Fix / Fix all | Present only when `ui.features.codeReviewButton` is on | After Code Review |
+
+```text
+› 2 file changes Undo All · Keep All · Review | Code Review (21)
+```
+
+- Left count = what **this chat** changed.
+- `Code Review (21)` = analyze **all** dirty git files (staged + unstaged + untracked as reported by `buildReviewDiff`).
+
+## Feature gate
+
+`Settings → Features → Code Review` (`mitii.ui.features.codeReviewButton`):
+
+- **Off:** bar shows chat file changes + Review. No Code Review CTA, no Findings tab, no Fix.
+- **On:** Code Review CTA with git count; findings appear after a review run with criticality + Fix.
+
+## Modules
+
+| File | Role |
+|---|---|
+| `reviewBarModel.ts` | Pure scope resolution (MUST stay host-free) |
+| `WorkingTreeReviewBar.tsx` | Orchestrator |
+| `ReviewBarActions.tsx` | Undo / Keep / Review / Code Review / Fix |
+| `ReviewFilesPanel.tsx` | Chat file list |
+| `ReviewFindingsPanel.tsx` | Finding chips by severity |
+
+## Tests
+
+`webview-ui/tests/reviewBarModel.test.ts` — scope visibility, labels, and gating.
diff --git a/apps/vscode/webview-ui/src/review/ReviewBarActions.tsx b/apps/vscode/webview-ui/src/review/ReviewBarActions.tsx
new file mode 100644
index 00000000..93940daa
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/ReviewBarActions.tsx
@@ -0,0 +1,142 @@
+type Props = {
+ reviewComplete: boolean;
+ canUndoKeep: boolean;
+ canFix: boolean;
+ canExpandReview: boolean;
+ showCodeReview: boolean;
+ codeReviewButtonLabel: string;
+ canRunCodeReview: boolean;
+ running: boolean;
+ hasFindings: boolean;
+ onUndoAll?: () => void;
+ onKeepAll?: () => void;
+ onDismissFindings?: () => void;
+ onFixAllFindings?: () => void;
+ onShowChanges: () => void;
+ onRunCodeReview?: () => void;
+ onCollapse: () => void;
+ onSelectFilesTab: () => void;
+};
+
+/** Action cluster: Undo/Keep (chat) · Review (chat) · Code Review (git). */
+export function ReviewBarActions({
+ reviewComplete,
+ canUndoKeep,
+ canFix,
+ canExpandReview,
+ showCodeReview,
+ codeReviewButtonLabel,
+ canRunCodeReview,
+ running,
+ hasFindings,
+ onUndoAll,
+ onKeepAll,
+ onDismissFindings,
+ onFixAllFindings,
+ onShowChanges,
+ onRunCodeReview,
+ onCollapse,
+ onSelectFilesTab,
+}: Props) {
+ if (reviewComplete) {
+ return (
+
+ {onDismissFindings ? (
+ {
+ onDismissFindings();
+ onCollapse();
+ }}
+ >
+ Clear
+
+ ) : null}
+
+ );
+ }
+
+ return (
+
+ onUndoAll?.()}
+ >
+ Undo All
+
+ {
+ onKeepAll?.();
+ onCollapse();
+ }}
+ >
+ Keep All
+
+ {hasFindings && onDismissFindings ? (
+ {
+ onDismissFindings();
+ onCollapse();
+ }}
+ >
+ Dismiss
+
+ ) : null}
+ {canFix ? (
+ onFixAllFindings?.()}
+ >
+ Fix all
+
+ ) : null}
+ {
+ onSelectFilesTab();
+ onShowChanges();
+ }}
+ >
+ Review
+
+ {showCodeReview && onRunCodeReview ? (
+
+ {running ? 'Reviewing…' : codeReviewButtonLabel}
+
+ ) : null}
+
+ );
+}
diff --git a/apps/vscode/webview-ui/src/review/ReviewFilesPanel.tsx b/apps/vscode/webview-ui/src/review/ReviewFilesPanel.tsx
new file mode 100644
index 00000000..a33c3c6f
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/ReviewFilesPanel.tsx
@@ -0,0 +1,53 @@
+import type { ReviewBarFile } from './reviewBarModel';
+import { statusLabel, statusTone } from './reviewBarFormat';
+
+type Props = {
+ files: readonly ReviewBarFile[];
+ onOpenDiff: (path: string) => void;
+ onOpenFile: (path: string) => void;
+};
+
+/** Files tab — always this-chat Mitii edits, never the full git tree. */
+export function ReviewFilesPanel({ files, onOpenDiff, onOpenFile }: Props) {
+ if (files.length === 0) {
+ return (
+
+ No Mitii edits in this chat yet. Code Review still covers the full git
+ working tree when enabled.
+
+ );
+ }
+
+ return (
+
+ {files.map((file) => {
+ const deleted = String(file.status).includes('D');
+ return (
+
+ onOpenDiff(file.path)}
+ title={`Open diff for ${file.path}`}
+ >
+
+ {statusLabel(String(file.status))}
+
+ {file.path}
+
+ onOpenFile(file.path)}
+ >
+ Open
+
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/vscode/webview-ui/src/review/ReviewFindingsPanel.tsx b/apps/vscode/webview-ui/src/review/ReviewFindingsPanel.tsx
new file mode 100644
index 00000000..78215839
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/ReviewFindingsPanel.tsx
@@ -0,0 +1,78 @@
+import type { ReviewFindingChip } from './reviewFindingTypes';
+import { severityTone } from './reviewBarFormat';
+
+type Props = {
+ findings: readonly ReviewFindingChip[];
+ running?: boolean;
+ onOpenFinding?: (path: string, line?: number) => void;
+ onOpenFile: (path: string) => void;
+ onFixFinding?: (index: number) => void;
+};
+
+/** Findings tab — severity chips + per-row Fix (Code Review feature only). */
+export function ReviewFindingsPanel({
+ findings,
+ running = false,
+ onOpenFinding,
+ onOpenFile,
+ onFixFinding,
+}: Props) {
+ if (findings.length === 0) {
+ return (
+
+ No findings yet. Click Code Review to analyze the git working-tree
+ changes.
+
+ );
+ }
+
+ return (
+
+ {findings.map((f, i) => {
+ const fixed = f.status === 'fixed';
+ return (
+
+ {
+ if (onOpenFinding) onOpenFinding(f.path, f.startLine);
+ else onOpenFile(f.path);
+ }}
+ title={`Open ${f.path}${f.startLine ? `:${f.startLine}` : ''}`}
+ >
+
+ {fixed ? 'fixed' : f.severity}
+
+
+ {f.path}
+ {f.startLine ? `:${f.startLine}` : ''}
+
+ {f.content}
+
+ {!fixed && onFixFinding ? (
+ onFixFinding(i)}
+ >
+ Fix
+
+ ) : fixed ? (
+
+ ✓
+
+ ) : null}
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/vscode/webview-ui/src/review/WorkingTreeReviewBar.tsx b/apps/vscode/webview-ui/src/review/WorkingTreeReviewBar.tsx
new file mode 100644
index 00000000..f3640a7e
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/WorkingTreeReviewBar.tsx
@@ -0,0 +1,223 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import type { ReviewDiffView, RunFileChangesView } from '../protocol';
+import { ReviewBarActions } from './ReviewBarActions';
+import { ReviewFilesPanel } from './ReviewFilesPanel';
+import { ReviewFindingsPanel } from './ReviewFindingsPanel';
+import type { ReviewFindingChip } from './reviewFindingTypes';
+import {
+ chatFilesFromRunChanges,
+ gitFilesFromReview,
+ resolveReviewBarScope,
+} from './reviewBarModel';
+
+export type { ReviewFindingChip } from './reviewFindingTypes';
+
+interface WorkingTreeReviewBarProps {
+ review: ReviewDiffView | null;
+ findings?: readonly ReviewFindingChip[];
+ /** Latest Mitii run edits — Files list + Undo All / Keep All. */
+ runChanges?: RunFileChangesView | null;
+ running?: boolean;
+ /** Increment to force-expand (command palette / Review CTA). */
+ expandSignal?: number;
+ onRefresh: () => void;
+ onOpenFile: (path: string) => void;
+ onOpenDiff: (path: string) => void;
+ onOpenFinding?: (path: string, line?: number) => void;
+ /** Expand the bar and show this-chat file changes (no LLM). */
+ onShowChanges: () => void;
+ /** LLM code review of the full git working tree. */
+ onRunCodeReview?: () => void;
+ showCodeReview?: boolean;
+ onUndoAll?: () => void;
+ onKeepAll?: () => void;
+ onDismissFindings?: () => void;
+ onFixFinding?: (index: number) => void;
+ onFixAllFindings?: () => void;
+}
+
+/**
+ * Flush review strip attached to the top of the composer box.
+ * Review = this-chat diffs. Code Review = full git + findings (feature-gated).
+ */
+export function WorkingTreeReviewBar({
+ review,
+ findings = [],
+ runChanges = null,
+ running = false,
+ expandSignal = 0,
+ onRefresh,
+ onOpenFile,
+ onOpenDiff,
+ onOpenFinding,
+ onShowChanges,
+ onRunCodeReview,
+ showCodeReview = false,
+ onUndoAll,
+ onKeepAll,
+ onDismissFindings,
+ onFixFinding,
+ onFixAllFindings,
+}: WorkingTreeReviewBarProps) {
+ const scope = useMemo(
+ () =>
+ resolveReviewBarScope({
+ gitFiles: gitFilesFromReview(review?.files),
+ chatFiles: chatFilesFromRunChanges(runChanges?.files),
+ findingsCount: findings.length,
+ showCodeReview,
+ running,
+ }),
+ [review?.files, runChanges?.files, findings.length, showCodeReview, running],
+ );
+
+ const [expanded, setExpanded] = useState(false);
+ const [tab, setTab] = useState<'files' | 'findings'>('files');
+
+ const openFindings = findings.filter((f) => f.status !== 'fixed');
+ const fixedFindings = findings.filter((f) => f.status === 'fixed');
+ const reviewComplete =
+ scope.showFindingsUi &&
+ findings.length > 0 &&
+ openFindings.length === 0 &&
+ fixedFindings.length > 0;
+ const hasOpenFindings = scope.showFindingsUi && openFindings.length > 0;
+
+ useEffect(() => {
+ if (expandSignal > 0) setExpanded(true);
+ }, [expandSignal]);
+
+ useEffect(() => {
+ if (scope.showFindingsUi && findings.length > 0) {
+ setTab('findings');
+ setExpanded(true);
+ }
+ }, [findings.length, scope.showFindingsUi]);
+
+ useEffect(() => {
+ if (reviewComplete) setExpanded(false);
+ }, [reviewComplete]);
+
+ if (!scope.visible) return null;
+
+ const canUndoKeep =
+ !reviewComplete && Boolean(runChanges && runChanges.files.length > 0);
+ const canFix =
+ hasOpenFindings && !running && Boolean(onFixAllFindings) && !reviewComplete;
+
+ return (
+
+
+ setExpanded((v) => !v)}
+ >
+
+ ›
+
+ {scope.summaryLabel}
+ {reviewComplete ? (
+
+ ✓
+
+ ) : null}
+ {hasOpenFindings ? (
+
+ {openFindings.length} open
+
+ ) : null}
+ {reviewComplete ? (
+ Done
+ ) : fixedFindings.length > 0 && scope.showFindingsUi ? (
+
+ {fixedFindings.length} fixed
+
+ ) : null}
+
+ 0}
+ onUndoAll={onUndoAll}
+ onKeepAll={onKeepAll}
+ onDismissFindings={
+ scope.showFindingsUi ? onDismissFindings : undefined
+ }
+ onFixAllFindings={scope.showFindingsUi ? onFixAllFindings : undefined}
+ onShowChanges={() => {
+ setExpanded(true);
+ onShowChanges();
+ }}
+ onRunCodeReview={onRunCodeReview}
+ onCollapse={() => setExpanded(false)}
+ onSelectFilesTab={() => setTab('files')}
+ />
+
+ {expanded ? (
+
+
+
+ setTab('files')}
+ >
+ Files
+
+ {scope.showFindingsUi ? (
+ setTab('findings')}
+ >
+ Findings{findings.length ? ` (${findings.length})` : ''}
+
+ ) : null}
+
+
+ Refresh
+
+
+ {tab === 'findings' && scope.showFindingsUi ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
+
+ );
+}
diff --git a/apps/vscode/webview-ui/src/review/index.ts b/apps/vscode/webview-ui/src/review/index.ts
new file mode 100644
index 00000000..30e38811
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/index.ts
@@ -0,0 +1,32 @@
+/**
+ * Composer review strip — dual-scope contract.
+ *
+ * | Surface | Scope | LLM |
+ * |---|---|---|
+ * | Summary count + Files tab + Review | This chat's Mitii run edits | No |
+ * | Code Review (N) | Full git working tree (`N` files) | Yes |
+ * | Findings / Fix / severity | Only when Settings → Features → Code Review is on | After Code Review |
+ *
+ * Host still refreshes `ReviewDiffView` via `refreshReviewDiff` so the Code Review
+ * badge stays accurate. Undo All / Keep All bind only to `RunFileChangesView`.
+ *
+ * Pure model: `reviewBarModel.ts` (unit-tested). UI shells stay under 300 lines.
+ */
+
+export { WorkingTreeReviewBar } from './WorkingTreeReviewBar';
+export { ComposerReviewStrip } from './ComposerReviewStrip';
+export type { ReviewFindingChip } from './reviewFindingTypes';
+export {
+ resolveReviewBarScope,
+ chatFilesFromRunChanges,
+ gitFilesFromReview,
+} from './reviewBarModel';
+export type {
+ ReviewBarFile,
+ ReviewBarScope,
+ ReviewBarScopeInput,
+} from './reviewBarModel';
+export {
+ selectLatestRunChanges,
+ composerNeedsReviewStrip,
+} from './selectLatestRunChanges';
diff --git a/apps/vscode/webview-ui/src/review/reviewBarFormat.ts b/apps/vscode/webview-ui/src/review/reviewBarFormat.ts
new file mode 100644
index 00000000..8284b395
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/reviewBarFormat.ts
@@ -0,0 +1,31 @@
+/** Display helpers for git status and finding severity chips. */
+
+export function statusLabel(status: string): string {
+ const normalized = status.trim() || '?';
+ if (normalized.includes('A') || normalized === 'A') return 'Added';
+ if (normalized.includes('D')) return 'Deleted';
+ if (normalized.includes('R')) return 'Renamed';
+ if (normalized.includes('?')) return 'Untracked';
+ if (normalized.includes('M') || normalized === 'M') return 'Edited';
+ return normalized;
+}
+
+export function statusTone(status: string): string {
+ const normalized = status.trim();
+ if (
+ normalized.includes('A') ||
+ normalized.includes('?') ||
+ normalized === 'A'
+ ) {
+ return 'added';
+ }
+ if (normalized.includes('D')) return 'deleted';
+ return 'edited';
+}
+
+export function severityTone(severity: string): string {
+ const s = severity.toLowerCase();
+ if (s === 'critical' || s === 'high') return 'high';
+ if (s === 'medium') return 'medium';
+ return 'low';
+}
diff --git a/apps/vscode/webview-ui/src/review/reviewBarModel.ts b/apps/vscode/webview-ui/src/review/reviewBarModel.ts
new file mode 100644
index 00000000..c3ef41b4
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/reviewBarModel.ts
@@ -0,0 +1,104 @@
+/**
+ * Dual-scope contract for the composer Working Tree review strip.
+ *
+ * - Review / Files list → this chat's Mitii run edits (`chatFiles`)
+ * - Code Review (N) → full git working tree (`gitFiles`), feature-gated
+ * - Findings / Fix → only when Code Review feature is enabled
+ */
+
+export type ReviewBarFile = {
+ path: string;
+ status: string;
+};
+
+export type ReviewBarScopeInput = {
+ gitFiles: readonly ReviewBarFile[];
+ chatFiles: readonly ReviewBarFile[];
+ findingsCount: number;
+ showCodeReview: boolean;
+ running?: boolean;
+};
+
+export type ReviewBarScope = {
+ /** Files shown under Review / Files (this chat only). */
+ chatFiles: ReviewBarFile[];
+ chatFileCount: number;
+ /** Working-tree file count for the Code Review CTA. */
+ gitFileCount: number;
+ summaryLabel: string;
+ visible: boolean;
+ canExpandReview: boolean;
+ canRunCodeReview: boolean;
+ showFindingsUi: boolean;
+ codeReviewButtonLabel: string;
+};
+
+function pluralFiles(count: number, noun: string): string {
+ return `${count} ${noun}${count === 1 ? '' : 's'}`;
+}
+
+/**
+ * Resolve what the review bar shows and which actions are enabled.
+ * Pure — safe to unit-test without React or the host bridge.
+ */
+export function resolveReviewBarScope(
+ input: ReviewBarScopeInput,
+): ReviewBarScope {
+ const chatFiles = input.chatFiles.map((f) => ({
+ path: f.path,
+ status: f.status,
+ }));
+ const chatFileCount = chatFiles.length;
+ const gitFileCount = input.gitFiles.length;
+ const showFindingsUi = input.showCodeReview === true;
+ const running = input.running === true;
+
+ // Hide when there is nothing to review — do not show "0 file changes"
+ // just because a run is in progress.
+ const visible =
+ chatFileCount > 0 ||
+ (showFindingsUi && input.findingsCount > 0) ||
+ (showFindingsUi && gitFileCount > 0);
+
+ let summaryLabel: string;
+ if (chatFileCount > 0) {
+ summaryLabel = pluralFiles(chatFileCount, 'file change');
+ } else if (showFindingsUi && gitFileCount > 0) {
+ summaryLabel = pluralFiles(gitFileCount, 'git change');
+ } else if (showFindingsUi && input.findingsCount > 0) {
+ summaryLabel = 'Review findings';
+ } else {
+ summaryLabel = pluralFiles(0, 'file change');
+ }
+
+ return {
+ chatFiles,
+ chatFileCount,
+ gitFileCount,
+ summaryLabel,
+ visible,
+ canExpandReview: chatFileCount > 0,
+ canRunCodeReview: showFindingsUi && gitFileCount > 0 && !running,
+ showFindingsUi,
+ codeReviewButtonLabel:
+ gitFileCount > 0
+ ? `Code Review (${gitFileCount})`
+ : 'Code Review',
+ };
+}
+
+/** Map run file-change entries into the bar's file DTO. */
+export function chatFilesFromRunChanges(
+ files: readonly { path: string; status: string }[] | null | undefined,
+): ReviewBarFile[] {
+ if (!files?.length) return [];
+ return files.map((f) => ({ path: f.path, status: String(f.status) }));
+}
+
+/** Map git review-diff entries into the bar's file DTO. */
+export function gitFilesFromReview(
+ files: readonly { path: string; status: string }[] | null | undefined,
+): ReviewBarFile[] {
+ if (!files?.length) return [];
+ return files.map((f) => ({ path: f.path, status: String(f.status) }));
+}
diff --git a/apps/vscode/webview-ui/src/review/reviewFindingTypes.ts b/apps/vscode/webview-ui/src/review/reviewFindingTypes.ts
new file mode 100644
index 00000000..4b22e4c3
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/reviewFindingTypes.ts
@@ -0,0 +1,12 @@
+/** Finding chip DTO shared by the review bar panels. */
+export type ReviewFindingChip = {
+ path: string;
+ content: string;
+ startLine?: number;
+ endLine?: number;
+ severity: string;
+ category?: string;
+ existingCode?: string;
+ suggestionCode?: string;
+ status?: 'open' | 'fixed';
+};
diff --git a/apps/vscode/webview-ui/src/review/selectLatestRunChanges.ts b/apps/vscode/webview-ui/src/review/selectLatestRunChanges.ts
new file mode 100644
index 00000000..c313bdb7
--- /dev/null
+++ b/apps/vscode/webview-ui/src/review/selectLatestRunChanges.ts
@@ -0,0 +1,29 @@
+import type { RunFileChangesView } from '../protocol';
+
+type TurnWithFileChanges = {
+ fileChanges?: RunFileChangesView;
+};
+
+/** Latest Mitii run file-changes attached to a chat turn (newest first). */
+export function selectLatestRunChanges(
+ turns: readonly TurnWithFileChanges[],
+): RunFileChangesView | null {
+ for (let i = turns.length - 1; i >= 0; i -= 1) {
+ const changes = turns[i]?.fileChanges;
+ if (changes?.files?.length) return changes;
+ }
+ return null;
+}
+
+/** Whether the composer should reserve space for the review strip. */
+export function composerNeedsReviewStrip(input: {
+ chatFileCount: number;
+ gitFileCount: number;
+ findingsCount: number;
+ codeReviewEnabled: boolean;
+}): boolean {
+ if (input.chatFileCount > 0) return true;
+ if (input.findingsCount > 0 && input.codeReviewEnabled) return true;
+ if (input.codeReviewEnabled && input.gitFileCount > 0) return true;
+ return false;
+}
diff --git a/apps/vscode/webview-ui/src/styles.css b/apps/vscode/webview-ui/src/styles.css
index ba64ddd7..ed4b9727 100644
--- a/apps/vscode/webview-ui/src/styles.css
+++ b/apps/vscode/webview-ui/src/styles.css
@@ -570,10 +570,9 @@ body.vscode-high-contrast .empty-state__logo--dark {
.timeline {
position: relative;
- --timeline-ecg-wave: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='180' viewBox='0 0 26 180'%3E%3Cpath d='M13 0 V12 L13 16 L3 21 L23 27 L13 33 V48 C13 54 13 57 13 61 L8 66 L17 70 L5 77 L22 84 L13 90 V108 C13 113 13 116 13 120 L1 126 L14 132 L10 137 L25 144 L13 151 V166 L13 170 L6 173 L20 177 L13 180' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
list-style: none;
margin: 6px 0 10px;
- padding: 3px 0 3px 28px;
+ padding: 3px 0 3px 22px;
display: flex;
flex-direction: column;
gap: 18px;
@@ -595,31 +594,16 @@ body.vscode-high-contrast .empty-state__logo--dark {
z-index: 1;
top: -3px;
bottom: -18px;
- left: -41px;
- width: 26px;
- background: var(--turn-accent, var(--mitii-accent));
- -webkit-mask-image: var(--timeline-ecg-wave);
- -webkit-mask-repeat: repeat-y;
- -webkit-mask-position: center 0;
- -webkit-mask-size: 26px 180px;
- mask-image: var(--timeline-ecg-wave);
- mask-repeat: repeat-y;
- mask-position: center 0;
- mask-size: 26px 180px;
+ left: -16px;
+ width: 1.5px;
+ border-radius: 999px;
+ background: color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 22%,
+ var(--mitii-border-soft)
+ );
pointer-events: none;
- transition:
- background 220ms ease,
- filter 220ms ease;
-}
-
-.timeline__row:nth-child(3n + 2)::before {
- -webkit-mask-position: center -60px;
- mask-position: center -60px;
-}
-
-.timeline__row:nth-child(3n)::before {
- -webkit-mask-position: center -120px;
- mask-position: center -120px;
+ transition: background 220ms ease, opacity 220ms ease;
}
.timeline__row:last-child::before {
@@ -627,35 +611,42 @@ body.vscode-high-contrast .empty-state__logo--dark {
}
.timeline__row--done::before {
- background: var(--turn-accent, var(--mitii-accent));
+ background: color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 48%,
+ var(--mitii-border-soft)
+ );
}
.timeline__row--active::before,
.timeline__row--thinking-active::before {
- background: var(--turn-accent, var(--mitii-accent));
- filter: drop-shadow(
- 0 0 3px color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 62%, transparent)
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 12%, transparent) 0%,
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 72%, transparent) 45%,
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 12%, transparent) 100%
);
- animation: timeline-ecg-active-step 1.1s ease-in-out infinite;
+ background-size: 100% 220%;
+ animation: timeline-rail-breathe 2.8s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
.timeline__row--warn::before {
- background: var(--turn-accent, var(--mitii-accent));
+ background: color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 42%,
+ var(--mitii-border-soft)
+ );
}
-@keyframes timeline-ecg-active-step {
+@keyframes timeline-rail-breathe {
0%,
100% {
- opacity: 0.58;
- filter: drop-shadow(
- 0 0 2px color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 48%, transparent)
- );
+ background-position: 0 0%;
+ opacity: 0.72;
}
- 45% {
+ 50% {
+ background-position: 0 100%;
opacity: 1;
- filter: drop-shadow(
- 0 0 6px color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 82%, transparent)
- );
}
}
@@ -681,26 +672,35 @@ body.vscode-high-contrast .empty-state__logo--dark {
.timeline__marker {
position: absolute;
z-index: 3;
- left: -30px;
- top: 0.55em;
- width: 4px;
- height: 4px;
+ left: -18.25px;
+ top: 0.5em;
+ width: 6px;
+ height: 6px;
border-radius: 999px;
- background: var(--turn-accent, var(--mitii-accent));
+ background: color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 78%,
+ var(--mitii-panel)
+ );
box-shadow: 0 0 0 3px var(--mitii-panel);
- transition: background 160ms ease;
+ transition:
+ background 180ms ease,
+ box-shadow 180ms ease,
+ transform 180ms ease;
}
.timeline__row--done .timeline__marker {
background: var(--turn-accent, var(--mitii-accent));
}
-.timeline__row--active .timeline__marker {
+.timeline__row--active .timeline__marker,
+.timeline__row--thinking-active .timeline__marker {
background: var(--turn-accent, var(--mitii-accent));
box-shadow:
0 0 0 3px var(--mitii-panel),
- 0 0 0 4px color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 16%, transparent);
- animation: timeline-pulse 1.6s ease-in-out infinite;
+ 0 0 0 5px
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 14%, transparent);
+ animation: timeline-marker-breathe 2.4s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
.timeline__row--warn .timeline__marker {
@@ -708,31 +708,39 @@ body.vscode-high-contrast .empty-state__logo--dark {
}
.timeline__row--tool .timeline__marker {
- top: 0.55em;
+ top: 0.5em;
}
.timeline__row--thinking-active .timeline__marker {
- top: 0.55em;
- background: var(--turn-accent, var(--mitii-accent));
- box-shadow: 0 0 0 3px var(--mitii-panel);
- animation: timeline-pulse 1.6s ease-in-out infinite;
+ top: 0.5em;
}
-@keyframes timeline-pulse {
+@keyframes timeline-marker-breathe {
0%,
100% {
- opacity: 1;
+ transform: scale(1);
+ box-shadow:
+ 0 0 0 3px var(--mitii-panel),
+ 0 0 0 5px
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 12%, transparent);
}
50% {
- opacity: 0.4;
+ transform: scale(1.08);
+ box-shadow:
+ 0 0 0 3px var(--mitii-panel),
+ 0 0 0 8px
+ color-mix(in srgb, var(--turn-accent, var(--mitii-accent)) 8%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.timeline__row--active::before,
- .timeline__row--thinking-active::before {
+ .timeline__row--thinking-active::before,
+ .timeline__row--active .timeline__marker,
+ .timeline__row--thinking-active .timeline__marker {
animation: none;
opacity: 1;
+ transform: none;
}
}
@@ -1042,18 +1050,18 @@ body.vscode-high-contrast .empty-state__logo--dark {
}
.live-status__dot {
- width: 7px;
- height: 7px;
+ width: 6px;
+ height: 6px;
border-radius: 999px;
flex: 0 0 auto;
background: var(--turn-accent, var(--mitii-accent));
- box-shadow: 0 0 0 3px
+ box-shadow: 0 0 0 4px
color-mix(
in srgb,
- var(--turn-accent, var(--mitii-accent)) 20%,
+ var(--turn-accent, var(--mitii-accent)) 12%,
transparent
);
- animation: timeline-pulse 1.4s ease-in-out infinite;
+ animation: live-status-breathe 2.4s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
}
.live-status__text {
@@ -1075,10 +1083,35 @@ body.vscode-high-contrast .empty-state__logo--dark {
}
}
+@keyframes live-status-breathe {
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: 0.78;
+ box-shadow: 0 0 0 4px
+ color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 10%,
+ transparent
+ );
+ }
+ 50% {
+ transform: scale(1.12);
+ opacity: 1;
+ box-shadow: 0 0 0 7px
+ color-mix(
+ in srgb,
+ var(--turn-accent, var(--mitii-accent)) 6%,
+ transparent
+ );
+ }
+}
+
.live-status__dots {
display: inline-flex;
- gap: 2px;
+ gap: 3px;
flex: 0 0 auto;
+ align-items: center;
}
.live-status__dots span {
@@ -1087,28 +1120,40 @@ body.vscode-high-contrast .empty-state__logo--dark {
border-radius: 999px;
background: color-mix(
in srgb,
- var(--turn-accent, var(--mitii-accent)) 66%,
+ var(--turn-accent, var(--mitii-accent)) 55%,
var(--mitii-muted)
);
- animation: live-status-blink 1.2s ease-in-out infinite;
+ animation: live-status-ellipsis 1.6s ease-in-out infinite;
}
.live-status__dots span:nth-child(2) {
- animation-delay: 0.15s;
+ animation-delay: 0.18s;
}
.live-status__dots span:nth-child(3) {
- animation-delay: 0.3s;
+ animation-delay: 0.36s;
}
-@keyframes live-status-blink {
+@keyframes live-status-ellipsis {
0%,
80%,
100% {
- opacity: 0.25;
+ opacity: 0.28;
+ transform: translateY(0);
}
40% {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .live-status__dot,
+ .live-status__dots span,
+ .live-status__text {
+ animation: none;
opacity: 1;
+ transform: none;
}
}
@@ -3778,9 +3823,12 @@ select.depth-select {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 28px;
+ min-width: 28px;
+ gap: 5px;
+ white-space: nowrap;
+ font-size: 11px;
height: 28px;
- padding: 0;
+ padding: 0 7px;
border-radius: 6px;
border: 1px solid var(--mitii-border-soft);
background: transparent;
@@ -3791,6 +3839,13 @@ select.depth-select {
background 120ms ease;
}
+.indexing-chip--icon {
+ width: 28px;
+ min-width: 28px;
+ padding: 0;
+ gap: 0;
+}
+
.indexing-chip:hover {
border-color: color-mix(in srgb, var(--index-tone) 40%, var(--mitii-border));
background: color-mix(in srgb, var(--index-tone) 8%, transparent);
@@ -5396,6 +5451,12 @@ button.md-file-link.md-inline-code:hover {
background: color-mix(in srgb, var(--mitii-ok) 18%, var(--mitii-panel));
}
+.wt-review__cta--code {
+ border-color: color-mix(in srgb, #38bdf8 45%, var(--mitii-border-soft));
+ color: var(--mitii-text);
+ background: color-mix(in srgb, #38bdf8 14%, var(--mitii-panel));
+}
+
.wt-review__cta:hover:not(:disabled) {
background: color-mix(in srgb, var(--mitii-surface) 90%, var(--mitii-panel));
}
@@ -5404,6 +5465,10 @@ button.md-file-link.md-inline-code:hover {
background: color-mix(in srgb, var(--mitii-ok) 28%, var(--mitii-panel));
}
+.wt-review__cta--code:hover:not(:disabled) {
+ background: color-mix(in srgb, #38bdf8 24%, var(--mitii-panel));
+}
+
.wt-review__cta:disabled {
opacity: 0.5;
cursor: default;
diff --git a/apps/vscode/webview-ui/tests/WorkingTreeReviewBar.test.tsx b/apps/vscode/webview-ui/tests/WorkingTreeReviewBar.test.tsx
new file mode 100644
index 00000000..c5379eba
--- /dev/null
+++ b/apps/vscode/webview-ui/tests/WorkingTreeReviewBar.test.tsx
@@ -0,0 +1,132 @@
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { WorkingTreeReviewBar } from '../src/review/WorkingTreeReviewBar';
+
+afterEach(() => cleanup());
+
+describe('WorkingTreeReviewBar', () => {
+ const chatFiles = {
+ runId: 'run-1',
+ files: [
+ { path: 'chat-a.ts', status: 'M' as const, additions: 1, deletions: 0 },
+ { path: 'chat-b.ts', status: 'A' as const, additions: 2, deletions: 0 },
+ ],
+ totalAdditions: 3,
+ totalDeletions: 0,
+ };
+
+ const review = {
+ summary: '## main',
+ files: [
+ { path: 'chat-a.ts', status: 'M' },
+ { path: 'chat-b.ts', status: 'A' },
+ { path: 'extra.ts', status: 'M' },
+ ],
+ };
+
+ it('lists this-chat files under Review and labels Code Review with git count', () => {
+ const onRunCodeReview = vi.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByText('2 file changes')).toBeTruthy();
+ fireEvent.click(screen.getByRole('button', { name: 'Review' }));
+ expect(screen.getByText('chat-a.ts')).toBeTruthy();
+ expect(screen.getByText('chat-b.ts')).toBeTruthy();
+ expect(screen.queryByText('extra.ts')).toBeNull();
+
+ const codeReview = screen.getByRole('button', {
+ name: 'Code Review (3)',
+ });
+ fireEvent.click(codeReview);
+ expect(onRunCodeReview).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders nothing when there are 0 file changes', () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ expect(screen.queryByLabelText('Working tree review')).toBeNull();
+ });
+
+ it('hides Findings and Code Review when the feature is off', () => {
+ render(
+ ,
+ );
+
+ expect(screen.queryByRole('button', { name: /Code Review/ })).toBeNull();
+ fireEvent.click(screen.getByRole('button', { name: 'Review' }));
+ expect(screen.queryByRole('button', { name: /Findings/ })).toBeNull();
+ expect(screen.queryByText('Bug')).toBeNull();
+ });
+
+ it('shows findings with severity and Fix when Code Review is enabled', () => {
+ const onFixFinding = vi.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByText('critical')).toBeTruthy();
+ expect(screen.getByText('Null deref')).toBeTruthy();
+ fireEvent.click(screen.getByRole('button', { name: 'Fix' }));
+ expect(onFixFinding).toHaveBeenCalledWith(0);
+ expect(screen.getByRole('button', { name: 'Fix all' })).toBeTruthy();
+ });
+});
diff --git a/apps/vscode/webview-ui/tests/reviewBarModel.test.ts b/apps/vscode/webview-ui/tests/reviewBarModel.test.ts
new file mode 100644
index 00000000..51cadcc1
--- /dev/null
+++ b/apps/vscode/webview-ui/tests/reviewBarModel.test.ts
@@ -0,0 +1,165 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ chatFilesFromRunChanges,
+ gitFilesFromReview,
+ resolveReviewBarScope,
+} from '../src/review/reviewBarModel';
+import {
+ composerNeedsReviewStrip,
+ selectLatestRunChanges,
+} from '../src/review/selectLatestRunChanges';
+import { severityTone, statusLabel, statusTone } from '../src/review/reviewBarFormat';
+
+describe('resolveReviewBarScope', () => {
+ const chat = [{ path: 'a.ts', status: 'M' }, { path: 'b.ts', status: 'A' }];
+ const git = [
+ ...chat,
+ { path: 'c.ts', status: 'M' },
+ { path: 'd.ts', status: '?' },
+ ];
+
+ it('summarizes this-chat file changes and keeps Code Review on full git', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: chat,
+ gitFiles: git,
+ findingsCount: 0,
+ showCodeReview: true,
+ });
+ expect(scope.summaryLabel).toBe('2 file changes');
+ expect(scope.chatFileCount).toBe(2);
+ expect(scope.gitFileCount).toBe(4);
+ expect(scope.codeReviewButtonLabel).toBe('Code Review (4)');
+ expect(scope.canExpandReview).toBe(true);
+ expect(scope.canRunCodeReview).toBe(true);
+ expect(scope.showFindingsUi).toBe(true);
+ expect(scope.visible).toBe(true);
+ });
+
+ it('hides Code Review findings UI when the feature is off', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: chat,
+ gitFiles: git,
+ findingsCount: 3,
+ showCodeReview: false,
+ });
+ expect(scope.showFindingsUi).toBe(false);
+ expect(scope.canRunCodeReview).toBe(false);
+ expect(scope.visible).toBe(true);
+ expect(scope.summaryLabel).toBe('2 file changes');
+ });
+
+ it('stays visible for git-only dirty trees when Code Review is enabled', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: [],
+ gitFiles: git,
+ findingsCount: 0,
+ showCodeReview: true,
+ });
+ expect(scope.visible).toBe(true);
+ expect(scope.summaryLabel).toBe('4 git changes');
+ expect(scope.canExpandReview).toBe(false);
+ expect(scope.codeReviewButtonLabel).toBe('Code Review (4)');
+ });
+
+ it('hides the bar when there are no chat edits and Code Review is off', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: [],
+ gitFiles: git,
+ findingsCount: 0,
+ showCodeReview: false,
+ });
+ expect(scope.visible).toBe(false);
+ });
+
+ it('hides the bar for 0 file changes even while a run is in progress', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: [],
+ gitFiles: [],
+ findingsCount: 0,
+ showCodeReview: false,
+ running: true,
+ });
+ expect(scope.visible).toBe(false);
+ });
+
+ it('disables Code Review while a run is in progress', () => {
+ const scope = resolveReviewBarScope({
+ chatFiles: chat,
+ gitFiles: git,
+ findingsCount: 0,
+ showCodeReview: true,
+ running: true,
+ });
+ expect(scope.canRunCodeReview).toBe(false);
+ expect(scope.visible).toBe(true);
+ });
+});
+
+describe('file mappers', () => {
+ it('maps chat and git DTOs', () => {
+ expect(chatFilesFromRunChanges([{ path: 'x.ts', status: 'M' }])).toEqual([
+ { path: 'x.ts', status: 'M' },
+ ]);
+ expect(gitFilesFromReview([{ path: 'y.ts', status: 'A' }])).toEqual([
+ { path: 'y.ts', status: 'A' },
+ ]);
+ expect(chatFilesFromRunChanges(null)).toEqual([]);
+ expect(gitFilesFromReview(undefined)).toEqual([]);
+ });
+});
+
+describe('selectLatestRunChanges', () => {
+ it('returns the newest turn with file changes', () => {
+ const latest = selectLatestRunChanges([
+ { fileChanges: { runId: 'r1', files: [{ path: 'old.ts', status: 'M', additions: 1, deletions: 0 }], totalAdditions: 1, totalDeletions: 0 } },
+ {},
+ { fileChanges: { runId: 'r2', files: [{ path: 'new.ts', status: 'A', additions: 2, deletions: 0 }], totalAdditions: 2, totalDeletions: 0 } },
+ ]);
+ expect(latest?.runId).toBe('r2');
+ });
+
+ it('returns null when no turns have edits', () => {
+ expect(selectLatestRunChanges([{}, { fileChanges: { runId: 'r', files: [], totalAdditions: 0, totalDeletions: 0 } }])).toBeNull();
+ });
+});
+
+describe('composerNeedsReviewStrip', () => {
+ it('reserves composer space for chat edits or gated git/findings', () => {
+ expect(
+ composerNeedsReviewStrip({
+ chatFileCount: 1,
+ gitFileCount: 0,
+ findingsCount: 0,
+ codeReviewEnabled: false,
+ }),
+ ).toBe(true);
+ expect(
+ composerNeedsReviewStrip({
+ chatFileCount: 0,
+ gitFileCount: 5,
+ findingsCount: 0,
+ codeReviewEnabled: false,
+ }),
+ ).toBe(false);
+ expect(
+ composerNeedsReviewStrip({
+ chatFileCount: 0,
+ gitFileCount: 5,
+ findingsCount: 0,
+ codeReviewEnabled: true,
+ }),
+ ).toBe(true);
+ });
+});
+
+describe('reviewBarFormat', () => {
+ it('maps status and severity tones', () => {
+ expect(statusLabel('M')).toBe('Edited');
+ expect(statusLabel('A')).toBe('Added');
+ expect(statusTone('D')).toBe('deleted');
+ expect(severityTone('critical')).toBe('high');
+ expect(severityTone('medium')).toBe('medium');
+ expect(severityTone('info')).toBe('low');
+ });
+});
diff --git a/apps/vscode/webview-ui/tests/settings.test.tsx b/apps/vscode/webview-ui/tests/settings.test.tsx
new file mode 100644
index 00000000..377e592e
--- /dev/null
+++ b/apps/vscode/webview-ui/tests/settings.test.tsx
@@ -0,0 +1,254 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import userEvent from '@testing-library/user-event';
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { createProviderHarness } from '../../tests/helpers/settingsHarness';
+import { App } from '../src/App';
+import { PROVIDER_OPTIONS } from '../src/providerOptions';
+import { LOOP_POLICY_FIELDS } from '../../src/loopPolicySettings';
+import { SETTINGS_FIELDS } from '../../src/settingsFields';
+
+const bridge = vi.hoisted(() => ({ post: vi.fn(), listener: undefined as any }));
+vi.mock('../src/bridge', () => ({
+ postToHost: bridge.post,
+ onHostMessage: (listener: any) => { bridge.listener = listener; return () => {}; },
+}));
+// Profile/MCP file storage is tested in host suites. Keep this bridge test in memory.
+vi.mock('../../src/profiles', async (original) => ({
+ ...await original(),
+ readProfiles: (_root: unknown, provider: unknown) => ({ activeProfileId: 'default', profiles: [{ id: 'default', name: 'Default', provider }] }),
+ writeProfiles: vi.fn(),
+}));
+vi.mock('../../src/mcpConfig', async (original) => ({
+ ...await original(), writeMcpSettings: vi.fn(),
+}));
+
+let harness: ReturnType;
+let host: any;
+let saves: Promise[];
+let mcp = { enabled: false, servers: [] };
+const emit = (message: any) => act(() => bridge.listener(message));
+async function hydrate() {
+ emit({ type: 'settings', provider: await host.readProvider(), autocomplete: host.readAutocomplete(),
+ search: await host.readSearch(), ui: host.readUi(), workspace: { root: '/tmp/workspace', rootOverride: harness.store.get('workspace.rootPathOverride') },
+ profiles: [], activeProfileId: 'default', mcp, mcpStore: [], mcpRuntimeStatus: 'disabled',
+ notice: {}, tokenUsage: host.tokenUsage });
+ emit({ type: 'index.status', index: { fileCount: 12, truncated: false,
+ maximumIndexFiles: harness.store.get('workspace.maximumIndexFiles') ?? 0,
+ embeddingSource: harness.store.get('semanticIndex.source') ?? 'bundled' } });
+}
+async function open(tab: string) { emit({ type: 'openSettings', tab }); }
+async function save() {
+ await userEvent.click(screen.getByRole('button', { name: 'Save', exact: true }));
+ await act(async () => { await Promise.all(saves); });
+ expect(screen.queryByText(/could not be saved/i)).toBeNull();
+}
+beforeEach(async () => {
+ vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} unobserve() {} });
+ Element.prototype.scrollIntoView = vi.fn();
+ harness = createProviderHarness(); host = harness.provider as any;
+ host.post = (message: any) => bridge.listener(message);
+ saves = []; mcp = { enabled: false, servers: [] };
+ bridge.post.mockImplementation((message: any) => {
+ if (message.type === 'settings.set') {
+ if (message.mcp) mcp = message.mcp;
+ saves.push(host.applySettingsSet(message).then(async () => {
+ bridge.listener({ type: 'settings.saved', ok: true });
+ await hydrate();
+ }));
+ }
+ });
+ render( );
+ await hydrate();
+});
+afterEach(() => { cleanup(); vi.unstubAllGlobals(); bridge.post.mockReset(); });
+
+describe('settings inputs through the host save/read bridge', () => {
+ it('keeps every embedding choice through status updates, save, and reopen', async () => {
+ await open('workspace');
+ for (const source of ['ollama', 'openai-compatible', 'disabled', 'bundled']) {
+ fireEvent.change(screen.getByLabelText('Embedding source'), { target: { value: source } });
+ emit({ type: 'index.status', index: { fileCount: 25, truncated: false, embeddingSource: 'bundled' } });
+ expect((screen.getByLabelText('Embedding source') as HTMLSelectElement).value).toBe(source);
+ await save();
+ expect(harness.store.get('semanticIndex.source')).toBe(source);
+ await open('model'); await open('workspace');
+ expect((screen.getByLabelText('Embedding source') as HTMLSelectElement).value).toBe(source);
+ }
+ });
+
+ it('tests without a model and requires an explicit model on save', async () => {
+ harness.store.set('provider.type', 'openai-compatible');
+ harness.store.set('provider.preset', 'custom');
+ harness.store.set('provider.model', '');
+ await hydrate(); await open('model');
+ expect((screen.getByLabelText('Model', { exact: true }) as HTMLSelectElement).value).toBe('');
+ expect(screen.queryByPlaceholderText('custom model id')).toBeNull();
+ fireEvent.click(screen.getByRole('button', { name: 'Test connection' }));
+ expect(bridge.post).toHaveBeenCalledWith(expect.objectContaining({ type: 'provider.testConnection', provider: expect.objectContaining({ model: '' }) }));
+ emit({ type: 'provider.connectionResult', ok: true, message: 'Connected. Choose a model.', models: ['test-model'] });
+ const before = saves.length;
+ fireEvent.click(screen.getByRole('button', { name: 'Save', exact: true }));
+ expect(saves.length).toBe(before);
+ expect(screen.getByText(/Choose a model before saving/)).toBeTruthy();
+ fireEvent.change(screen.getByLabelText('Model', { exact: true }), { target: { value: 'test-model' } });
+ await save();
+ expect(harness.store.get('provider.model')).toBe('test-model');
+ });
+});
+
+// Hidden legacy/advanced fields use the host matrix; the UI exposes Simple budget sliders instead.
+const inputFields = SETTINGS_FIELDS.filter(field =>
+ !['provider.type', 'provider.preset', 'semanticIndex.source', 'semanticIndex.backend', 'semanticIndex.enabled', 'mcp.enabled', 'developer.intensityOverrides', 'ui.effort'].includes(field.id)
+ && (!field.id.startsWith('tokenBudget.') || field.id === 'tokenBudget.enabled')
+ && (!field.id.startsWith('loopPolicy.') || field.id === 'loopPolicy.enabled' || LOOP_POLICY_FIELDS.some(f => `loopPolicy.${f.key}` === field.id && f.tier !== 'advanced'))
+ && !/ui\.modeDefaults\..*\.(depth|thoroughness)$/.test(field.id));
+const labels: Record = {
+ 'autocomplete.enabled': 'Enable autocomplete',
+ 'runBudget.unlimited': 'Unlimited run budget',
+};
+describe('each editable settings input saves and restores its value', () => {
+ it.each(inputFields.map(field => [field.id, field] as const))('%s', async (id, field) => {
+ harness.store.set('developer.enabled', true);
+ harness.store.set('tokenBudget.enabled', true);
+ harness.store.set('loopPolicy.enabled', true);
+ if (id === 'provider.baseUrl') { harness.store.set('provider.type', 'openai-compatible'); harness.store.set('provider.preset', 'custom'); }
+ await hydrate(); await open(field.tab);
+ if (id.startsWith('ui.modeDefaults.')) {
+ const mode = id.split('.')[2];
+ fireEvent.click(screen.getByRole('tab', { name: mode[0].toUpperCase() + mode.slice(1), exact: true }));
+ }
+ document.querySelectorAll('details').forEach(el => { el.open = true; });
+ let label = labels[id] ?? field.label;
+ if (id.endsWith('.approvalMode')) label = 'Approval mode';
+ if (/ui\.modeDefaults\..*\.model$/.test(id)) label = 'Default model';
+ const getInput = () => screen.getByLabelText(label, { exact: true }) as HTMLInputElement | HTMLSelectElement;
+ const input = getInput();
+ let expected: string | boolean = String(field.sample);
+ if (input instanceof HTMLSelectElement) {
+ // Exercise each option, including returning to the original value.
+ for (const option of Array.from(input.options).filter(o => o.value !== '__custom__' && (o.value || id !== 'provider.model'))) {
+ fireEvent.change(getInput(), { target: { value: option.value } });
+ await save();
+ expect(getInput().value, `${id} option ${option.value}`).toBe(option.value);
+ expect(harness.store.get(id), id).toBe(option.value);
+ }
+ return;
+ }
+ if (input.type === 'checkbox') {
+ expected = !input.checked;
+ fireEvent.click(input);
+ } else {
+ if (String(expected) === input.value) {
+ const value = Number(expected);
+ const step = field.kind === 'number' ? 0.01 : 1;
+ expected = input.type === 'number' ? String(value + (value + step <= (field.max ?? Infinity) ? step : -step)) : `${expected}-edited`;
+ }
+ fireEvent.focus(input);
+ fireEvent.change(input, { target: { value: expected } });
+ fireEvent.blur(input);
+ }
+ await save();
+ expect(harness.store.get(id), id).toEqual(input.type === 'checkbox' ? expected : field.kind === 'int' || field.kind === 'number' ? Number(expected) : expected);
+ // Remount from persisted host snapshots; local React state cannot make this pass.
+ cleanup(); render( ); await hydrate(); await open(field.tab);
+ if (id.startsWith('ui.modeDefaults.')) {
+ const mode = id.split('.')[2];
+ fireEvent.click(screen.getByRole('tab', { name: mode[0].toUpperCase() + mode.slice(1), exact: true }));
+ }
+ expect(input.type === 'checkbox' ? (getInput() as HTMLInputElement).checked : getInput().value).toEqual(expected);
+ });
+});
+
+
+describe('simple budget controls and remaining settings', () => {
+ it.each([
+ ['filesPerMutation', 7], ['outputRatio', 25], ['repositoryShare', 31],
+ ['conversationShare', 33], ['planShare', 9], ['skillsShare', 7], ['verificationChecks', 5],
+ ])('saves the %s slider and number input', async (key, edited) => {
+ harness.store.set('developer.enabled', true);
+ await hydrate(); await open('debug');
+ const id = `tokenBudget.${key}`;
+ fireEvent.change(document.getElementById(id)!, { target: { value: String(edited) } });
+ await save();
+ cleanup(); render( ); await hydrate(); await open('debug');
+ expect((document.getElementById(id) as HTMLInputElement).value).toBe(String(edited));
+ const number = document.getElementById(id)!.parentElement!.querySelector('input[type=number]')!;
+ fireEvent.focus(number);
+ fireEvent.change(number, { target: { value: String(Number(edited) + 1) } });
+ fireEvent.blur(number);
+ await save();
+ expect((document.getElementById(id) as HTMLInputElement).value).toBe(String(Number(edited) + 1));
+ });
+
+ it('saves SearXNG URL and restores it', async () => {
+ await open('model');
+ fireEvent.change(screen.getByLabelText('SearXNG base URL'), { target: { value: 'http://localhost:8888' } });
+ await save();
+ expect(harness.store.get('search.searxngBaseUrl')).toBe('http://localhost:8888');
+ expect((screen.getByLabelText('SearXNG base URL') as HTMLInputElement).value).toBe('http://localhost:8888');
+ });
+
+ it('saves both states of the MCP switch', async () => {
+ await open('integrations');
+ for (const enabled of [true, false]) {
+ fireEvent.click(screen.getByLabelText('Enable MCP'));
+ await save();
+ expect(mcp.enabled).toBe(enabled);
+ expect((screen.getByLabelText('Enable MCP') as HTMLInputElement).checked).toBe(enabled);
+ }
+ });
+});
+
+
+describe('provider choices and indexing status', () => {
+ it.each(PROVIDER_OPTIONS.map(p => [p.preset, p] as const))('saves and restores the %s provider', async (_id, preset) => {
+ await open('model');
+ fireEvent.change(document.getElementById('ptype')!, { target: { value: preset.preset } });
+ if (!preset.model) {
+ expect((screen.getByLabelText('Model', { exact: true }) as HTMLSelectElement).value).toBe('');
+ emit({ type: 'provider.connectionResult', ok: true, message: 'Connected', models: ['chosen-model'] });
+ fireEvent.change(screen.getByLabelText('Model', { exact: true }), { target: { value: 'chosen-model' } });
+ }
+ await save();
+ expect(harness.store.get('provider.preset')).toBe(preset.preset);
+ expect(harness.store.get('provider.type')).toBe(preset.type);
+ expect((document.getElementById('ptype') as HTMLSelectElement).value).toBe(preset.preset);
+ expect((screen.getByLabelText('Model', { exact: true }) as HTMLSelectElement).value).toBe(preset.model || 'chosen-model');
+ });
+ it('shows exact discovered and published file counts separately', async () => {
+ await open('workspace');
+ emit({ type: 'index.status', index: { fileCount: 12, discoveredFileCount: 1234, progressStage: 'indexing', readiness: 'indexing', truncated: true, message: 'Indexing code and text' } });
+ expect(screen.getByText('Indexed files')).toBeTruthy();
+ expect(screen.getByText('Files discovered in current scan')).toBeTruthy();
+ expect(screen.getByText('1234')).toBeTruthy();
+ expect(screen.getByText('Yes — scan is incomplete')).toBeTruthy();
+ // Header chip is icon-only; file counts live in Workspace settings.
+ expect(screen.queryByText('1,234 files')).toBeNull();
+ });
+ it('keeps the file limit draft through a progress update', async () => {
+ await open('workspace');
+ const input = screen.getByLabelText('Maximum index files');
+ fireEvent.change(input, { target: { value: '12345' } }); fireEvent.blur(input);
+ emit({ type: 'index.status', index: { fileCount: 12, truncated: false, maximumIndexFiles: 0 } });
+ await save();
+ expect(harness.store.get('workspace.maximumIndexFiles')).toBe(12345);
+ expect((screen.getByLabelText('Maximum index files') as HTMLInputElement).value).toBe('12345');
+ });
+});
+
+
+it('saves a manually entered custom model ID', async () => {
+ await open('model');
+ fireEvent.change(screen.getByLabelText('Model', { exact: true }), { target: { value: '__custom__' } });
+ fireEvent.change(screen.getByLabelText('Custom model ID'), { target: { value: 'my-custom-model' } });
+ await save();
+ expect(harness.store.get('provider.model')).toBe('my-custom-model');
+});
+
+it('allows Full access to be selected while running', async () => {
+ emit({ type: 'run.started', mode: 'agent', prompt: 'test access' });
+ await userEvent.click(screen.getByRole('button', { name: 'Approval', exact: true }));
+ await userEvent.click(screen.getByRole('option', { name: /Full access/ }));
+ await act(async () => { await Promise.all(saves); });
+ expect(harness.store.get('safety.approvalMode')).toBe('pilot');
+});
diff --git a/apps/vscode/webview-ui/tests/vscode.ts b/apps/vscode/webview-ui/tests/vscode.ts
new file mode 100644
index 00000000..f5eab7ee
--- /dev/null
+++ b/apps/vscode/webview-ui/tests/vscode.ts
@@ -0,0 +1,2 @@
+// VS Code is provided by the shared Vitest setup.
+export {};
diff --git a/apps/vscode/webview-ui/vitest.config.ts b/apps/vscode/webview-ui/vitest.config.ts
new file mode 100644
index 00000000..43cbb909
--- /dev/null
+++ b/apps/vscode/webview-ui/vitest.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vitest/config';
+import { resolve } from 'node:path';
+
+export default defineConfig({
+ esbuild: { jsx: 'automatic' },
+ resolve: { alias: {
+ vscode: resolve(__dirname, 'tests/vscode.ts'),
+ '@mitii/live-token-budget': resolve(__dirname, '../src/liveTokenBudgetPreview.ts'),
+ '@mitii/v8': resolve(__dirname, '../../../packages/v8/src/index.ts'),
+ '@mitii/sdk': resolve(__dirname, '../../../packages/sdk/src/index.ts'),
+ '@mitii/host': resolve(__dirname, '../../../packages/host/src/index.ts'),
+ '@mitii/search-kit': resolve(__dirname, '../../../packages/search-kit/src/index.ts'),
+ } },
+ test: {
+ environment: 'jsdom',
+ include: ['tests/**/*.test.{ts,tsx}'],
+ setupFiles: ['../../../vitest.setup.ts'],
+ testTimeout: 30000,
+ },
+});
diff --git a/package.json b/package.json
index 70cd6683..8c76650d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mitii-ai-agent",
"description": "Private Mitii monorepo workspace orchestrator. Product packages: @mitii/v8, @mitii/sdk, @mitii/automation, @mitii/search-kit, @mitii/mcp, @mitii/mcp-web, @mitii/host, @mitii/cli, @mitii/daemon, @mitii/acp, apps/vscode.",
- "version": "2.9.57",
+ "version": "2.9.66",
"private": true,
"license": "AGPL-3.0-or-later",
"author": {
@@ -47,7 +47,7 @@
"typecheck:daemon": "pnpm --filter @mitii/daemon typecheck",
"typecheck:acp": "pnpm --filter @mitii/acp typecheck",
"typecheck:vscode": "pnpm --filter ./apps/vscode typecheck",
- "test": "node scripts/ensure-node-native.mjs && vitest run",
+ "test": "node scripts/ensure-node-native.mjs && vitest run && pnpm run test:webview",
"test:watch": "node scripts/ensure-node-native.mjs && vitest",
"test:v8": "pnpm --filter @mitii/v8 test",
"test:sdk": "pnpm --filter @mitii/sdk test",
@@ -89,11 +89,13 @@
"log-viewer": "node tools/log-viewer/server.mjs",
"audit:dependencies": "node scripts/audit-dependencies.mjs",
"audit:engines": "node scripts/audit-package-engines.mjs",
- "check:circular-deps": "node scripts/check-circular-deps.mjs"
+ "check:circular-deps": "node scripts/check-circular-deps.mjs",
+ "test:webview": "pnpm --filter @mitii/vscode-webview test"
},
"devDependencies": {
"@electron/rebuild": "^4.2.0",
"@types/node": "^20.14.0",
+ "jsdom": "^26.1.0",
"knip": "^6.21.0",
"madge": "^8.0.0",
"typescript": "^5.5.2",
diff --git a/packages/automation/package.json b/packages/automation/package.json
index 1d31bbb8..e1ec55ea 100644
--- a/packages/automation/package.json
+++ b/packages/automation/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/automation",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Mitii automation control plane: schedules, event ingress, claim/lease runner, webhooks (Phases 1–2).",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/automation/src/index.ts b/packages/automation/src/index.ts
index bd4f8522..a4698fdb 100644
--- a/packages/automation/src/index.ts
+++ b/packages/automation/src/index.ts
@@ -41,7 +41,11 @@ export {
export type { ParsedCron } from './cron/next.js';
export { SqliteAutomationStore } from './store/sqliteStore.js';
-export type { UpsertSpecInput } from './store/sqliteStore.js';
+export type {
+ OpenAutomationSqliteDatabase,
+ SqliteAutomationStoreOptions,
+ UpsertSpecInput,
+} from './store/sqliteStore.js';
export { AUTOMATION_SCHEMA_STATEMENTS } from './store/schema.js';
export {
diff --git a/packages/automation/src/service.ts b/packages/automation/src/service.ts
index 54dc30a6..744d433a 100644
--- a/packages/automation/src/service.ts
+++ b/packages/automation/src/service.ts
@@ -18,7 +18,10 @@ import {
import { ClaimRunner, type ClaimRunnerEvent } from './runner/claimRunner.js';
import type { AutomationRunExecutor } from './runner/types.js';
import { reconcileCronSpecsDir } from './specs/reconciler.js';
-import { SqliteAutomationStore } from './store/sqliteStore.js';
+import {
+ SqliteAutomationStore,
+ type OpenAutomationSqliteDatabase,
+} from './store/sqliteStore.js';
import {
createScheduleInputSchema,
type AutomationRunRecord,
@@ -35,6 +38,8 @@ import {
export interface AutomationServiceOptions {
dbPath?: string;
+ /** VS Code / Electron hosts must inject the staged better-sqlite3 binding. */
+ openDatabase?: OpenAutomationSqliteDatabase;
executor?: AutomationRunExecutor;
pollIntervalMs?: number;
claimLeaseSeconds?: number;
@@ -56,7 +61,9 @@ export class AutomationService {
constructor(options: AutomationServiceOptions = {}) {
const dbPath = resolveAutomationDbPath({ dbPath: options.dbPath });
- this.store = new SqliteAutomationStore(dbPath);
+ this.store = new SqliteAutomationStore(dbPath, {
+ openDatabase: options.openDatabase,
+ });
this.options = options;
this.ingress = new EventIngress({ store: this.store });
}
diff --git a/packages/automation/src/store/sqliteStore.ts b/packages/automation/src/store/sqliteStore.ts
index a049f736..d25865ca 100644
--- a/packages/automation/src/store/sqliteStore.ts
+++ b/packages/automation/src/store/sqliteStore.ts
@@ -26,6 +26,16 @@ import { AUTOMATION_SCHEMA_STATEMENTS } from './schema.js';
type SqliteDb = Database.Database;
+/** Host-injected opener (VS Code passes Electron-native better-sqlite3 bindings). */
+export type OpenAutomationSqliteDatabase = (
+ filename: string,
+ options?: Database.Options,
+) => SqliteDb;
+
+export type SqliteAutomationStoreOptions = {
+ openDatabase?: OpenAutomationSqliteDatabase;
+};
+
function boolToInt(value: boolean): number {
return value ? 1 : 0;
}
@@ -149,10 +159,13 @@ export class SqliteAutomationStore {
readonly db: SqliteDb;
readonly dbPath: string;
- constructor(dbPath: string) {
+ constructor(dbPath: string, options: SqliteAutomationStoreOptions = {}) {
mkdirSync(dirname(dbPath), { recursive: true });
this.dbPath = dbPath;
- this.db = new Database(dbPath);
+ const open =
+ options.openDatabase ??
+ ((filename, openOptions) => new Database(filename, openOptions));
+ this.db = open(dbPath);
this.db.pragma('journal_mode = WAL');
this.db.pragma('foreign_keys = ON');
for (const statement of AUTOMATION_SCHEMA_STATEMENTS) {
diff --git a/packages/host/package.json b/packages/host/package.json
index 5ce1fc24..e601c9d3 100644
--- a/packages/host/package.json
+++ b/packages/host/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/host",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Shared host kit for Mitii apps: SQLite injection, workspace indexing, repository context, durable ports (checkpoints/memory/skills/search/network), project rules, provider presets. Web retrieval via @mitii/search-kit.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/host/src/config/createHostLlmPorts.spec.ts b/packages/host/src/config/createHostLlmPorts.spec.ts
index cd7740e1..0843785a 100644
--- a/packages/host/src/config/createHostLlmPorts.spec.ts
+++ b/packages/host/src/config/createHostLlmPorts.spec.ts
@@ -99,6 +99,27 @@ describe('resolveProviderApiKey', () => {
});
describe('testProviderConnection', () => {
+ it.each(['openai-compatible', 'anthropic', 'gemini'])('discovers %s models before selection without a generation request', async (type) => {
+ const requests: string[] = [];
+ const result = await testProviderConnection({
+ type, baseUrl: 'https://provider.example', model: '', apiKey: 'test-key',
+ fetchImpl: (async (url: RequestInfo | URL, init?: RequestInit) => {
+ requests.push(String(url));
+ expect(init?.method ?? 'GET').toBe('GET');
+ return new Response(JSON.stringify({ data: [{ id: 'test-model' }], models: [{ name: 'models/test-model' }] }));
+ }) as typeof fetch,
+ });
+ expect(result.ok).toBe(true);
+ expect(result.models).toEqual(['test-model']);
+ expect(result.message).toContain('Choose a model');
+ expect(requests).toHaveLength(1);
+ });
+
+ it('does not claim success when discovery fails without a model', async () => {
+ const result = await testProviderConnection({ type: 'openai-compatible', baseUrl: 'https://provider.example', model: '', fetchImpl: (async () => new Response('', { status: 401 })) as typeof fetch });
+ expect(result.ok).toBe(false);
+ });
+
it('accepts echo without a network call', async () => {
const result = await testProviderConnection({
type: 'echo',
diff --git a/packages/host/src/config/testProviderConnection.ts b/packages/host/src/config/testProviderConnection.ts
index 20fc93a1..f399558c 100644
--- a/packages/host/src/config/testProviderConnection.ts
+++ b/packages/host/src/config/testProviderConnection.ts
@@ -43,7 +43,10 @@ export async function testProviderConnection(
}
if (!model.trim()) {
- return { ok: false, message: 'Model is required.' };
+ const models = await listProviderModels(input);
+ return models.length > 0
+ ? { ok: true, message: 'Connected. Choose a model, then save settings.', models }
+ : { ok: false, message: 'Could not discover models. Check the endpoint and credentials, or enter a custom model to test.' };
}
if (type === 'anthropic') {
diff --git a/packages/host/src/indexing/bundled-embedding/adapters/OnnxRuntimeSessionFactory.ts b/packages/host/src/indexing/bundled-embedding/adapters/OnnxRuntimeSessionFactory.ts
index c51e5977..c3eb77ad 100644
--- a/packages/host/src/indexing/bundled-embedding/adapters/OnnxRuntimeSessionFactory.ts
+++ b/packages/host/src/indexing/bundled-embedding/adapters/OnnxRuntimeSessionFactory.ts
@@ -150,33 +150,46 @@ export class HostOnnxRuntimeSessionFactory implements OnnxRuntimeSessionFactory
input.abortSignal?.throwIfAborted();
const preferred = resolvePreferredOnnxKind(input.preferredKind);
const tryNative = preferred !== 'wasm';
+ const nativeErrors: string[] = [];
if (tryNative) {
const native = this.loadModule(ONNX_RUNTIME_NODE_PACKAGE);
if (native?.InferenceSession && native.Tensor) {
- const raw = await native.InferenceSession.create(input.modelPath, {
- executionProviders: ['cpu'],
- });
- return {
- session: wrapSession(raw),
- resolution: describeOnnxExecutionProvider({
- kind: 'native',
- packageId: ONNX_RUNTIME_NODE_PACKAGE,
- }),
- createInt64Tensor: (values, dims) =>
- new native.Tensor!(
- 'int64',
- BigInt64Array.from(values.map((value) => BigInt(value))),
- [...dims],
- ),
- };
+ try {
+ const raw = await native.InferenceSession.create(input.modelPath, {
+ executionProviders: ['cpu'],
+ });
+ return {
+ session: wrapSession(raw),
+ resolution: describeOnnxExecutionProvider({
+ kind: 'native',
+ packageId: ONNX_RUNTIME_NODE_PACKAGE,
+ }),
+ createInt64Tensor: (values, dims) =>
+ new native.Tensor!(
+ 'int64',
+ BigInt64Array.from(values.map((value) => BigInt(value))),
+ [...dims],
+ ),
+ };
+ } catch (error) {
+ // ABI / native create failures are common in Extension Host — fall
+ // through to WASM so bundled embeddings still work.
+ nativeErrors.push(
+ error instanceof Error ? error.message : String(error),
+ );
+ }
}
}
const wasm = this.loadModule(ONNX_RUNTIME_WEB_PACKAGE);
if (!wasm?.InferenceSession || !wasm.Tensor) {
+ const nativeHint =
+ nativeErrors.length > 0
+ ? ` Native attempt failed: ${nativeErrors.join('; ')}.`
+ : '';
throw new Error(
- 'ONNX Runtime is unavailable. Install optional native modules onnxruntime-node (preferred) or onnxruntime-web (WASM fallback) for this OS/CPU.',
+ `ONNX Runtime is unavailable. Install optional native modules onnxruntime-node (preferred) or onnxruntime-web (WASM fallback) for this OS/CPU.${nativeHint}`,
);
}
configureWasmPaths(wasm, ONNX_RUNTIME_WEB_PACKAGE);
diff --git a/packages/host/src/indexing/bundled-embedding/tests/OnnxRuntimeSessionFactory.spec.ts b/packages/host/src/indexing/bundled-embedding/tests/OnnxRuntimeSessionFactory.spec.ts
index 2348619d..700b294a 100644
--- a/packages/host/src/indexing/bundled-embedding/tests/OnnxRuntimeSessionFactory.spec.ts
+++ b/packages/host/src/indexing/bundled-embedding/tests/OnnxRuntimeSessionFactory.spec.ts
@@ -17,6 +17,52 @@ describe('HostOnnxRuntimeSessionFactory', () => {
}),
).rejects.toThrow(/onnx|no such file|not exist|FILE|PROTOBUF|unavailable/i);
});
+
+ it('falls back to WASM when native InferenceSession.create throws', async () => {
+ const modelPath = join(tmpdir(), 'mitii-fallback-minilm.onnx');
+ let wasmCreateCalls = 0;
+ const factory = new HostOnnxRuntimeSessionFactory((packageId) => {
+ if (packageId === 'onnxruntime-node') {
+ return {
+ Tensor: class {
+ constructor() {}
+ } as never,
+ InferenceSession: {
+ create: async () => {
+ throw new Error('NODE_MODULE_VERSION mismatch');
+ },
+ },
+ };
+ }
+ if (packageId === 'onnxruntime-web') {
+ return {
+ Tensor: class {
+ constructor() {}
+ } as never,
+ InferenceSession: {
+ create: async () => {
+ wasmCreateCalls += 1;
+ return {
+ inputNames: ['input_ids'],
+ outputNames: ['last_hidden_state'],
+ run: async () => ({}),
+ };
+ },
+ },
+ env: { wasm: {} },
+ };
+ }
+ return undefined;
+ });
+
+ // Force native-first so the create() throw path is exercised, then WASM.
+ const created = await factory.create({
+ modelPath,
+ preferredKind: 'native',
+ });
+ expect(wasmCreateCalls).toBe(1);
+ expect(created.resolution.kind).toBe('wasm');
+ });
});
describe('resolvePreferredOnnxKind', () => {
diff --git a/packages/host/src/indexing/fingerprintSnapshot.ts b/packages/host/src/indexing/fingerprintSnapshot.ts
index 0c64d523..8c55e244 100644
--- a/packages/host/src/indexing/fingerprintSnapshot.ts
+++ b/packages/host/src/indexing/fingerprintSnapshot.ts
@@ -10,6 +10,7 @@ import {
} from '@mitii/v8';
import { WORKSPACE_WALK_SKIP_DIR_NAMES, shouldSkipWorkspaceWalkFile } from '../internal/workspaceWalk.js';
+import { DEFAULT_MAXIMUM_INDEX_FILES } from './indexLimits.js';
import { readIndexRuntimeMetadata } from './semanticIndex.js';
export interface WorkspaceSnapshotOptions {
@@ -55,7 +56,9 @@ export function fingerprintWorkspaceIndexSnapshot(
export async function buildWorkspaceSnapshot(
options: WorkspaceSnapshotOptions,
): Promise {
- const maxFiles = options.maxFiles ?? 2_000;
+ // Match the full-index default so a temporary host_snapshot fallback is not
+ // stuck at a tiny 2k pin while embeddings/code indexes are unavailable.
+ const maxFiles = options.maxFiles ?? DEFAULT_MAXIMUM_INDEX_FILES;
const entries: string[] = [];
const relativePaths: string[] = [];
let truncated = false;
diff --git a/packages/host/src/indexing/fullWorkspaceIndex.ts b/packages/host/src/indexing/fullWorkspaceIndex.ts
index 471dcf48..bd70d7fc 100644
--- a/packages/host/src/indexing/fullWorkspaceIndex.ts
+++ b/packages/host/src/indexing/fullWorkspaceIndex.ts
@@ -355,6 +355,15 @@ async function runFullWorkspaceIndexOnce(options: {
resolvedSemantic.status === 'ready' && vectorIndex.status === 'ready'
? resolvedSemantic.provider.profile.id
: 'unavailable',
+ ...(vectorIndex.status !== 'ready'
+ ? {
+ lastEmbeddingError:
+ vectorIndex.reason ??
+ (resolvedSemantic.status === 'unavailable'
+ ? resolvedSemantic.reason
+ : 'Embedding synchronization did not complete.'),
+ }
+ : {}),
snapshotFingerprint,
fileCount: snapshot.statistics.files,
truncated: snapshot.status !== 'complete',
diff --git a/packages/host/src/indexing/semanticIndex.ts b/packages/host/src/indexing/semanticIndex.ts
index 5b3ce0fa..096cafb6 100644
--- a/packages/host/src/indexing/semanticIndex.ts
+++ b/packages/host/src/indexing/semanticIndex.ts
@@ -111,6 +111,8 @@ export interface IndexRuntimeMetadata {
lanceDbPath: string;
embeddingProfile?: EmbeddingProfile;
vectorRuntimeKey?: string;
+ /** Last probe/sync failure reason when vectors are unavailable or degraded. */
+ lastEmbeddingError?: string;
snapshotFingerprint?: string;
fileCount?: number;
truncated?: boolean;
diff --git a/packages/host/src/workspaceKit.spec.ts b/packages/host/src/workspaceKit.spec.ts
index d0bd9d78..b4b89520 100644
--- a/packages/host/src/workspaceKit.spec.ts
+++ b/packages/host/src/workspaceKit.spec.ts
@@ -220,4 +220,20 @@ describe('host durable workspace kit', () => {
);
expect(snapshot.candidate.roots[0]?.rootId).toBe(root.split(/[\\/]/).pop());
});
+
+ it('honors an explicit fingerprint maxFiles override', async () => {
+ await writeFile(join(root, 'a.ts'), 'a', 'utf8');
+ await writeFile(join(root, 'b.ts'), 'b', 'utf8');
+ await writeFile(join(root, 'c.ts'), 'c', 'utf8');
+ const snapshot = await buildWorkspaceSnapshot({
+ workspaceRoot: root,
+ workspaceId: 'ws_1',
+ maxFiles: 2,
+ });
+ expect(snapshot.fileCount).toBe(2);
+ expect(snapshot.truncated).toBe(true);
+ expect(snapshot.candidate.reasons?.[0]?.message).toContain(
+ 'truncated after 2 files',
+ );
+ });
});
diff --git a/packages/mcp/package.json b/packages/mcp/package.json
index 31907166..3f5f4763 100644
--- a/packages/mcp/package.json
+++ b/packages/mcp/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/mcp",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Mitii MCP client kit: connect to MCP servers (stdio/SSE/streamable-HTTP) and register tools into V8 ToolRegistry. Does not expose Mitii as an MCP server.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/mcp/web/package.json b/packages/mcp/web/package.json
index 06be26ac..20961e05 100644
--- a/packages/mcp/web/package.json
+++ b/packages/mcp/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/mcp-web",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Mitii MCP stdio server under packages/mcp/web: web_search, fetch_url, optional memory_search via search-kit (no v8).",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index 5e9a35bf..6d4ff177 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/sdk",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Host-neutral Mitii programmatic API over @mitii/v8 Agent Engine.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/sdk/src/contracts.ts b/packages/sdk/src/contracts.ts
index 1c717b1b..3393124e 100644
--- a/packages/sdk/src/contracts.ts
+++ b/packages/sdk/src/contracts.ts
@@ -175,6 +175,10 @@ export const mitiiStartInputSchema = z
.array(z.string().min(1).max(64))
.max(MAX_REQUIRED_SKILLS)
.optional(),
+ excludedSkillIds: z
+ .array(z.string().min(1).max(64))
+ .max(20)
+ .optional(),
requiredMcpServerIds: z
.array(z.string().min(1).max(64))
.max(MAX_REQUIRED_MCP_SERVERS)
diff --git a/packages/search-kit/package.json b/packages/search-kit/package.json
index 65f9d2ea..7b650534 100644
--- a/packages/search-kit/package.json
+++ b/packages/search-kit/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/search-kit",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Mitii web retrieval kit: pluggable search providers, content resolvers, and URL safety. Host-neutral; no V8 dependency.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/v8/package.json b/packages/v8/package.json
index 2556cbe6..4128e487 100644
--- a/packages/v8/package.json
+++ b/packages/v8/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/v8",
- "version": "2.9.57",
+ "version": "2.9.66",
"description": "Host-neutral Mitii V8 agent runtime (modules + engine).",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/v8/src/engine/agent-engine/actions/buildStallContinueRationale.ts b/packages/v8/src/engine/agent-engine/actions/buildStallContinueRationale.ts
index 6f527330..34211675 100644
--- a/packages/v8/src/engine/agent-engine/actions/buildStallContinueRationale.ts
+++ b/packages/v8/src/engine/agent-engine/actions/buildStallContinueRationale.ts
@@ -74,23 +74,23 @@ export function buildBudgetWallRationale(params: {
case "exploration_stall":
lines.push(
zeroProgressMutation
- ? "The run stalled after repeated file re-reads without applying the required workspace edits."
- : "This task looks large and the run stalled after repeated file re-reads.",
+ ? "We've been looking around, but this task still needs a bit more research before we can start making changes."
+ : "This task looks larger than expected, and we could use a little more time to understand it.",
);
break;
case "unfulfilled_execute":
lines.push(
- "The run hit a mutation recovery limit without applying the required workspace edits.",
+ "We need a bit more research on this task before we can start implementing.",
);
break;
case "rejected_mutation":
lines.push(
- "The run could not land a valid workspace edit after rejected mutation attempts.",
+ "Our first edit attempts didn't land cleanly — we'd like to take another careful look before trying again.",
);
break;
case "incomplete_execute":
lines.push(
- "The execute run stopped with open change surfaces or a clear blocker before finishing.",
+ "We made some progress, but we're not quite finished yet and could use a little more time.",
);
break;
case "budget_exhausted":
@@ -134,18 +134,18 @@ export function buildBudgetWallRationale(params: {
params.reason === "incomplete_execute"
) {
lines.push(
- "Continue for a fresh approach (optionally narrow the task or point to files), or stop here.",
+ "Mind if we dig a little deeper first? You can also share any tips (like files to focus on), or we can stop here.",
);
} else if (params.reason === "budget_exhausted") {
lines.push(
- "Continue to extend the run budget once and keep working, or stop here with current progress.",
+ "Want us to keep going a bit longer, or stop here with what we have so far?",
);
} else if (params.reason === "verification_repair_capped") {
lines.push(
- "Continue for another verification repair pass, or stop here and keep the current changes.",
+ "Want us to take another pass at fixing the remaining issues, or stop here and keep the current changes?",
);
} else {
- lines.push("Continue to keep working, or stop here.");
+ lines.push("Want us to keep going, or stop here?");
}
return lines.join(" ");
diff --git a/packages/v8/src/engine/agent-engine/actions/tests/buildStallContinueRationale.spec.ts b/packages/v8/src/engine/agent-engine/actions/tests/buildStallContinueRationale.spec.ts
index ccd72010..0069165d 100644
--- a/packages/v8/src/engine/agent-engine/actions/tests/buildStallContinueRationale.spec.ts
+++ b/packages/v8/src/engine/agent-engine/actions/tests/buildStallContinueRationale.spec.ts
@@ -43,8 +43,8 @@ describe("buildBudgetWallRationale", () => {
changedFiles: [],
mutationRequired: true,
});
- expect(rationale).toContain("mutation recovery limit");
- expect(rationale).toContain("narrow the task");
+ expect(rationale).toContain("more research");
+ expect(rationale).toContain("dig a little deeper");
});
it("frames budget exhaustion with extend copy", () => {
@@ -54,7 +54,7 @@ describe("buildBudgetWallRationale", () => {
budgetMessage: "Model call budget exhausted.",
});
expect(rationale).toContain("Model call budget exhausted");
- expect(rationale).toContain("extend the run budget");
+ expect(rationale).toContain("keep going a bit longer");
});
it("frames verification repair caps", () => {
@@ -63,7 +63,7 @@ describe("buildBudgetWallRationale", () => {
changedFiles: ["src/a.ts"],
});
expect(rationale).toContain("Verification repairs are capped");
- expect(rationale).toContain("another verification repair pass");
+ expect(rationale).toContain("another pass at fixing");
});
});
@@ -76,8 +76,8 @@ describe("buildStallContinueRationale", () => {
mutationRequired: true,
});
- expect(rationale).toContain("without applying the required workspace edits");
- expect(rationale).toContain("narrow the task");
+ expect(rationale).toContain("more research");
+ expect(rationale).toContain("dig a little deeper");
});
});
diff --git a/packages/v8/src/engine/agent-engine/contracts/input/AgentEngineInput.ts b/packages/v8/src/engine/agent-engine/contracts/input/AgentEngineInput.ts
index a741ca04..9ab108a7 100644
--- a/packages/v8/src/engine/agent-engine/contracts/input/AgentEngineInput.ts
+++ b/packages/v8/src/engine/agent-engine/contracts/input/AgentEngineInput.ts
@@ -177,6 +177,11 @@ export const agentEngineStartInputSchema = z
* Explicitly attached skill ids for this run (@skill:, CLI --skill, host pin).
*/
requiredSkillIds: z.array(z.string().min(1).max(64)).max(3).default([]),
+ /**
+ * Skill ids that must not be auto-matched for this run.
+ * Explicit requiredSkillIds still win when listed there.
+ */
+ excludedSkillIds: z.array(z.string().min(1).max(64)).max(20).default([]),
/**
* Explicitly attached MCP server ids for this run (@mcp:, host pin).
* Empty = all enabled MCP servers under the grant (default).
diff --git a/packages/v8/src/engine/agent-engine/pipeline/executeStart.ts b/packages/v8/src/engine/agent-engine/pipeline/executeStart.ts
index 23fb96fa..385be422 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/executeStart.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/executeStart.ts
@@ -513,6 +513,7 @@ export async function executeStart(
memoryFacts,
establishedFacts,
requiredSkillIds: input.requiredSkillIds ?? [],
+ excludedSkillIds: input.excludedSkillIds ?? [],
selectedSkillIds: selectedSkills?.map((block) => block.id) ?? [],
evidence: runEvidence,
windowPolicy,
@@ -559,6 +560,7 @@ export async function executeStart(
projects: input.projects,
memoryFacts,
requiredSkillIds: input.requiredSkillIds ?? [],
+ excludedSkillIds: input.excludedSkillIds ?? [],
selectedSkillIds: selectedSkills?.map((block) => block.id) ?? [],
establishedFacts,
plan: shared.runPlan,
diff --git a/packages/v8/src/engine/agent-engine/pipeline/executeStartEnrichment.ts b/packages/v8/src/engine/agent-engine/pipeline/executeStartEnrichment.ts
index f2834ce6..d59d5051 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/executeStartEnrichment.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/executeStartEnrichment.ts
@@ -390,6 +390,7 @@ export async function runStartEnrichment(
budgetTokens: windowPolicy.skills.budgetTokens,
maxSkills: windowPolicy.skills.maxSkills,
requiredSkillIds: input.requiredSkillIds ?? [],
+ excludedSkillIds: input.excludedSkillIds ?? [],
forbidLargeSkills:
resolveWindowBudgetBand(windowPolicy.contextWindowTokens) === "compact",
evidence: {
diff --git a/packages/v8/src/engine/agent-engine/pipeline/executeToolSupport.ts b/packages/v8/src/engine/agent-engine/pipeline/executeToolSupport.ts
index 4c3c0600..250988a4 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/executeToolSupport.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/executeToolSupport.ts
@@ -205,6 +205,7 @@ export async function refreshAuthorityAfterTools(
route: ExecutionDecision["route"];
windowPolicy: WindowPolicy;
requiredSkillIds?: readonly string[];
+ excludedSkillIds?: readonly string[];
}): Promise {
const discoveredPaths = [
...new Set([
@@ -324,6 +325,7 @@ export async function refreshAuthorityAfterTools(
budgetTokens: params.windowPolicy.skills.budgetTokens,
maxSkills: params.windowPolicy.skills.maxSkills,
requiredSkillIds: [...(params.requiredSkillIds ?? [])],
+ excludedSkillIds: [...(params.excludedSkillIds ?? [])],
forbidLargeSkills:
resolveWindowBudgetBand(params.windowPolicy.contextWindowTokens) ===
"compact",
diff --git a/packages/v8/src/engine/agent-engine/pipeline/modelLoopToolPhase.ts b/packages/v8/src/engine/agent-engine/pipeline/modelLoopToolPhase.ts
index 93e16434..84a61fb8 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/modelLoopToolPhase.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/modelLoopToolPhase.ts
@@ -108,6 +108,7 @@ export async function runModelLoopToolPhase(params: {
mode: "ask" | "plan" | "agent" | undefined;
projects: readonly ProjectDescriptor[] | undefined;
requiredSkillIds: string[] | undefined;
+ excludedSkillIds: string[] | undefined;
answer: string;
changeImpactGate: { required: boolean; satisfied: boolean };
thresholds: AgentEngineThresholds;
@@ -145,6 +146,7 @@ export async function runModelLoopToolPhase(params: {
mode,
projects,
requiredSkillIds,
+ excludedSkillIds,
answer,
changeImpactGate,
thresholds,
@@ -553,6 +555,7 @@ export async function runModelLoopToolPhase(params: {
route: decision.route,
windowPolicy: windowPolicy,
requiredSkillIds: requiredSkillIds,
+ excludedSkillIds: excludedSkillIds,
});
if (grantExpansionOutcome.kind === "expansion_required") {
session.decision = decision;
diff --git a/packages/v8/src/engine/agent-engine/pipeline/modelToolLoop.ts b/packages/v8/src/engine/agent-engine/pipeline/modelToolLoop.ts
index 0d74c268..9661e33e 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/modelToolLoop.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/modelToolLoop.ts
@@ -101,6 +101,7 @@ export async function runModelToolLoop(
establishedFacts?: EstablishedFact[];
selectedSkillIds?: string[];
requiredSkillIds?: string[];
+ excludedSkillIds?: string[];
taskListRef: TaskListRef;
evidence?: RunEvidence;
windowPolicy: WindowPolicy;
@@ -593,6 +594,7 @@ export async function runModelToolLoop(
mode: params.mode,
projects: params.projects,
requiredSkillIds: params.requiredSkillIds,
+ excludedSkillIds: params.excludedSkillIds,
answer: session.answer,
changeImpactGate,
thresholds,
diff --git a/packages/v8/src/engine/agent-engine/pipeline/tests/tryOfferBudgetWallContinue.spec.ts b/packages/v8/src/engine/agent-engine/pipeline/tests/tryOfferBudgetWallContinue.spec.ts
index 443b3598..6ddbf1fc 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/tests/tryOfferBudgetWallContinue.spec.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/tests/tryOfferBudgetWallContinue.spec.ts
@@ -25,7 +25,7 @@ describe("tryOfferBudgetWallContinue", () => {
expect(outcome?.kind).toBe("continue_required");
expect(outcome?.wallReason).toBe("unfulfilled_execute");
- expect(outcome?.rationale).toContain("mutation recovery limit");
+ expect(outcome?.rationale).toContain("more research");
});
it("returns undefined when override cap is reached", () => {
diff --git a/packages/v8/src/engine/agent-engine/pipeline/verificationFinish.ts b/packages/v8/src/engine/agent-engine/pipeline/verificationFinish.ts
index 14d432e3..00a483aa 100644
--- a/packages/v8/src/engine/agent-engine/pipeline/verificationFinish.ts
+++ b/packages/v8/src/engine/agent-engine/pipeline/verificationFinish.ts
@@ -120,6 +120,7 @@ export async function finishAfterLoop(
memoryFacts?: readonly { id: string; content: string }[];
selectedSkillIds?: string[];
requiredSkillIds?: string[];
+ excludedSkillIds?: string[];
establishedFacts: EstablishedFact[];
plan?: PlanArtifact;
};
@@ -826,6 +827,7 @@ export async function finishAfterLoop(
establishedFacts: params.loopContext?.establishedFacts ?? [],
selectedSkillIds: params.loopContext?.selectedSkillIds,
requiredSkillIds: params.loopContext?.requiredSkillIds,
+ excludedSkillIds: params.loopContext?.excludedSkillIds,
evidence,
windowPolicy,
logVerbosity: input.logVerbosity,
diff --git a/packages/v8/src/engine/agent-engine/tests/AgentEngineStall.spec.ts b/packages/v8/src/engine/agent-engine/tests/AgentEngineStall.spec.ts
index 69e5f091..955c23eb 100644
--- a/packages/v8/src/engine/agent-engine/tests/AgentEngineStall.spec.ts
+++ b/packages/v8/src/engine/agent-engine/tests/AgentEngineStall.spec.ts
@@ -309,7 +309,7 @@ describe("AgentEnginePipeline stall and read dedup", () => {
expect(result.reasonCodes).toContain("stall_continue_suspended");
expect(result.reasonCodes).not.toContain("mutation_applied");
expect(result.suspension?.continuePrompt ?? "").toContain(
- "without applying the required workspace edits",
+ "more research before we can start making changes",
);
expect(result.answer ?? "").not.toContain("I still need the same file");
@@ -391,7 +391,7 @@ describe("AgentEnginePipeline stall and read dedup", () => {
expect(result.reasonCodes).toContain("stall_continue_suspended");
expect(result.reasonCodes).not.toContain("mutation_applied");
expect(result.suspension?.continuePrompt ?? "").toMatch(
- /read-only discovery|mutation recovery limit/i,
+ /more research|dig a little deeper/i,
);
expect(result.answer ?? "").not.toContain("Should not be reached");
});
@@ -683,7 +683,7 @@ describe("AgentEnginePipeline stall and read dedup", () => {
expect(result.reasonCodes).toContain("unfulfilled_execute_exhausted");
expect(result.reasonCodes).toContain("stall_continue_suspended");
expect(result.suspension?.continuePrompt ?? "").toMatch(
- /rejected mutation|valid workspace edit/i,
+ /didn't land cleanly|dig a little deeper/i,
);
expect(result.usage.modelCalls).toBe(2);
expect(result.answer ?? "").not.toContain("Should not be reached");
diff --git a/packages/v8/src/modules/decision-policy/actions/BuildToolGrant.ts b/packages/v8/src/modules/decision-policy/actions/BuildToolGrant.ts
index 4a881c9f..e297c434 100644
--- a/packages/v8/src/modules/decision-policy/actions/BuildToolGrant.ts
+++ b/packages/v8/src/modules/decision-policy/actions/BuildToolGrant.ts
@@ -109,10 +109,9 @@ export function buildToolGrant(params: {
) {
if (route === "diagnose") {
reasonCodes.push("diagnosis_readonly");
- if (
- understanding.intent.classification.primaryTaskIntent === "review" ||
- looksLikeCodeReviewRequest(params.message ?? "")
- ) {
+ // Structured findings only when the host/CLI injected review markers —
+ // never from free-form Ask/Plan/Agent text or a review intent label alone.
+ if (looksLikeCodeReviewRequest(params.message ?? "")) {
reasonCodes.push("review_pipeline_required");
reasonCodes.push("review_findings_structured");
}
diff --git a/packages/v8/src/modules/decision-policy/actions/ResolveRoute.ts b/packages/v8/src/modules/decision-policy/actions/ResolveRoute.ts
index e9d89ecd..fea22ee4 100644
--- a/packages/v8/src/modules/decision-policy/actions/ResolveRoute.ts
+++ b/packages/v8/src/modules/decision-policy/actions/ResolveRoute.ts
@@ -358,12 +358,12 @@ function resolveAskRoute(params: {
}): RouteResolution {
const { primary, taskAnalysis, message, reasonCodes } = params;
- // Host Review UI maps to ask mode; classifiers often label these as
- // "question" once review prep is prepended. Keep the diagnose + structured
- // findings path regardless.
- if (isDiagnosisIntent(primary) || looksLikeCodeReviewRequest(message)) {
+ // Structured review is button/CLI-prep only (host markers). Free-form
+ // "code review" questions stay normal diagnose/answer — never force findings.
+ const structuredReview = looksLikeCodeReviewRequest(message);
+ if (isDiagnosisIntent(primary) || structuredReview) {
reasonCodes.push("diagnosis_readonly");
- if (primary === "review" || looksLikeCodeReviewRequest(message)) {
+ if (structuredReview) {
reasonCodes.push("review_pipeline_required");
reasonCodes.push("review_findings_structured");
}
@@ -406,8 +406,8 @@ function resolveAskRoute(params: {
}
/**
- * Explicit code-review asks (VS Code Review mode, CLI, or free-form).
- * Matches host prefixes that include emit_review_finding / working-tree language.
+ * Structured git review is opt-in only: VS Code Review button / CLI prep.
+ * Matches host-injected markers — not free-form "code review" chat phrasing.
*/
export function looksLikeCodeReviewRequest(message: string): boolean {
const text = message.replace(/\nClarification:\s*[\s\S]*$/i, "").trim();
@@ -419,20 +419,6 @@ export function looksLikeCodeReviewRequest(message: string): boolean {
return true;
}
- if (
- /\b(?:code\s*review|review\s+(?:the\s+)?(?:current\s+)?(?:git\s+)?(?:working[- ]tree\s+)?(?:changes|diff|patch|pr|pull\s+request|commit))\b/i.test(
- text,
- )
- ) {
- return true;
- }
-
- if (
- /\breview\b[\s\S]{0,80}\b(?:bugs?|security|findings?|severity)\b/i.test(text)
- ) {
- return true;
- }
-
if (/^#\s*Review prep\b/m.test(text) || /\bReview prep\s*\(/i.test(text)) {
return true;
}
diff --git a/packages/v8/src/modules/decision-policy/actions/index.ts b/packages/v8/src/modules/decision-policy/actions/index.ts
index 50ccea76..3d81865c 100644
--- a/packages/v8/src/modules/decision-policy/actions/index.ts
+++ b/packages/v8/src/modules/decision-policy/actions/index.ts
@@ -1,6 +1,6 @@
export { toolGrantsEquivalent } from "./CompareToolGrants";
-export { resolveRoute, isMutationIntent, isDiagnosisIntent, looksLikeAgentVerificationRequest } from "./ResolveRoute";
+export { resolveRoute, isMutationIntent, isDiagnosisIntent, looksLikeAgentVerificationRequest, looksLikeCodeReviewRequest } from "./ResolveRoute";
export type { RouteResolution } from "./ResolveRoute";
export { looksLikeWorkspaceBugReport } from "./LooksLikeWorkspaceBugReport";
diff --git a/packages/v8/src/modules/decision-policy/tests/DecisionPolicyPipeline.spec.ts b/packages/v8/src/modules/decision-policy/tests/DecisionPolicyPipeline.spec.ts
index 0b397009..8ed89299 100644
--- a/packages/v8/src/modules/decision-policy/tests/DecisionPolicyPipeline.spec.ts
+++ b/packages/v8/src/modules/decision-policy/tests/DecisionPolicyPipeline.spec.ts
@@ -500,6 +500,50 @@ describe("DecisionPolicyPipeline", () => {
expect(decision.reasonCodes).toContain("mode_ask_readonly");
});
+ it("does not force structured review for free-form code-review questions", () => {
+ const decision = new DecisionPolicyPipeline().decide(
+ createInput({
+ mode: "ask",
+ message:
+ "can you show the code on how it detects the unused files for code review?",
+ understanding: createUnderstanding({
+ primaryTaskIntent: "review",
+ interactionIntent: "question",
+ taskAnalysis: {
+ scope: "repository",
+ clarity: "unclear",
+ recommendsRepositoryDiscovery: true,
+ },
+ }),
+ }),
+ );
+
+ expect(decision.route).toBe("diagnose");
+ expect(decision.reasonCodes).toContain("diagnosis_readonly");
+ expect(decision.reasonCodes).not.toContain("review_pipeline_required");
+ expect(decision.reasonCodes).not.toContain("review_findings_structured");
+ });
+
+ it("does not force structured review for free-form review phrasing without host markers", () => {
+ const decision = new DecisionPolicyPipeline().decide(
+ createInput({
+ mode: "ask",
+ message: "Review the current git changes for bugs",
+ understanding: createUnderstanding({
+ primaryTaskIntent: "review",
+ interactionIntent: "question",
+ taskAnalysis: {
+ scope: "multi_file",
+ recommendsRepositoryDiscovery: true,
+ },
+ }),
+ }),
+ );
+
+ expect(decision.reasonCodes).not.toContain("review_pipeline_required");
+ expect(decision.reasonCodes).not.toContain("review_findings_structured");
+ });
+
it("routes ask-mode project questions to repository_answer with read tools", () => {
const decision = new DecisionPolicyPipeline().decide(
createInput({
diff --git a/packages/v8/src/modules/decision-policy/tests/fixtures/goldenCases.ts b/packages/v8/src/modules/decision-policy/tests/fixtures/goldenCases.ts
index f3b1a4e1..dab75e0d 100644
--- a/packages/v8/src/modules/decision-policy/tests/fixtures/goldenCases.ts
+++ b/packages/v8/src/modules/decision-policy/tests/fixtures/goldenCases.ts
@@ -923,12 +923,14 @@ const GOLDEN_DECISION_CASES_CORE: GoldenDecisionCase[] = [
expected: {
route: "diagnose",
maximumWorkspaceEffect: "read",
- reasonCodesIncludes: [
- "diagnosis_readonly",
+ // Free-form review intent is diagnose-only — structured findings require
+ // Review-button / CLI prep markers (emit_review_finding / Review prep).
+ reasonCodesIncludes: ["diagnosis_readonly"],
+ reasonCodesExcludes: [
"review_pipeline_required",
"review_findings_structured",
],
- allowedToolsIncludes: ["emit_review_finding", "read_file", "read_git_status"],
+ allowedToolsIncludes: ["read_file", "read_git_status"],
allowedToolsExcludes: ["apply_patch"],
},
},
diff --git a/packages/v8/src/modules/repository-state/internal/embedding/SqliteEmbeddingVectorCache.ts b/packages/v8/src/modules/repository-state/internal/embedding/SqliteEmbeddingVectorCache.ts
index 537e1f46..b5ac779c 100644
--- a/packages/v8/src/modules/repository-state/internal/embedding/SqliteEmbeddingVectorCache.ts
+++ b/packages/v8/src/modules/repository-state/internal/embedding/SqliteEmbeddingVectorCache.ts
@@ -21,10 +21,16 @@ interface CacheRow {
dimensions: number;
}
+/**
+ * SQLite-backed embedding cache. Cache failures must never abort embedding
+ * synchronization — LanceDB is the source of truth for vectors.
+ */
export class SqliteEmbeddingVectorCache
implements EmbeddingVectorCachePort
{
private ready = false;
+ private disabled = false;
+ private readonly memory = new Map();
constructor(
private readonly database: SqliteDatabasePort,
@@ -34,11 +40,21 @@ export class SqliteEmbeddingVectorCache
profileId: string,
contentHash: string,
): readonly number[] | undefined {
- this.ensureSchema();
+ const memoryKey = cacheKey(profileId, contentHash);
+ const mem = this.memory.get(memoryKey);
+ if (mem) return mem;
- const row = this.database
- .prepare(
- `
+ if (this.disabled) {
+ return undefined;
+ }
+
+ try {
+ this.ensureSchema();
+ if (this.disabled) return undefined;
+
+ const row = this.database
+ .prepare(
+ `
SELECT
vector_json AS vectorJson,
dimensions AS dimensions
@@ -47,14 +63,13 @@ export class SqliteEmbeddingVectorCache
AND content_hash = ?
LIMIT 1
`,
- )
- .get(profileId, contentHash) as CacheRow | undefined;
+ )
+ .get(profileId, contentHash) as CacheRow | undefined;
- if (!row) {
- return undefined;
- }
+ if (!row) {
+ return undefined;
+ }
- try {
const parsed = JSON.parse(row.vectorJson) as unknown;
if (
!Array.isArray(parsed) ||
@@ -66,6 +81,7 @@ export class SqliteEmbeddingVectorCache
return parsed;
} catch {
+ this.disabled = true;
return undefined;
}
}
@@ -75,11 +91,20 @@ export class SqliteEmbeddingVectorCache
contentHash: string,
vector: readonly number[],
): void {
- this.ensureSchema();
+ const memoryKey = cacheKey(profileId, contentHash);
+ this.memory.set(memoryKey, vector);
+
+ if (this.disabled) {
+ return;
+ }
+
+ try {
+ this.ensureSchema();
+ if (this.disabled) return;
- this.database
- .prepare(
- `
+ this.database
+ .prepare(
+ `
INSERT INTO embedding_vector_cache (
profile_id,
content_hash,
@@ -94,22 +119,34 @@ export class SqliteEmbeddingVectorCache
vector_json = excluded.vector_json,
updated_at = excluded.updated_at
`,
- )
- .run(
- profileId,
- contentHash,
- vector.length,
- JSON.stringify(vector),
- Date.now(),
- );
+ )
+ .run(
+ profileId,
+ contentHash,
+ vector.length,
+ JSON.stringify(vector),
+ Date.now(),
+ );
+ } catch {
+ // Keep process-local memory cache; continue embedding without SQLite.
+ this.disabled = true;
+ }
}
private ensureSchema(): void {
- if (this.ready) {
+ if (this.ready || this.disabled) {
return;
}
- this.database.exec(CREATE_CACHE_TABLE);
- this.ready = true;
+ try {
+ this.database.exec(CREATE_CACHE_TABLE);
+ this.ready = true;
+ } catch {
+ this.disabled = true;
+ }
}
}
+
+function cacheKey(profileId: string, contentHash: string): string {
+ return `${profileId}\0${contentHash}`;
+}
diff --git a/packages/v8/src/modules/repository-state/internal/embedding/constants.ts b/packages/v8/src/modules/repository-state/internal/embedding/constants.ts
index 0a06c583..1a4f8bd7 100644
--- a/packages/v8/src/modules/repository-state/internal/embedding/constants.ts
+++ b/packages/v8/src/modules/repository-state/internal/embedding/constants.ts
@@ -41,8 +41,10 @@ export const EMBEDDING_DEFAULTS = {
MAXIMUM_ALLOWED_CHANGES_PER_BATCH:
10_000,
+ // High enough that a first full index of ~30k files can finish embeddings
+ // in one run instead of staying chronically "degraded"/partial.
MAXIMUM_BATCHES_PER_RUN:
- 100,
+ 2_000,
MAXIMUM_ALLOWED_BATCHES_PER_RUN:
10_000,
diff --git a/packages/v8/src/modules/repository-state/internal/embedding/tests/SqliteEmbeddingVectorCache.spec.ts b/packages/v8/src/modules/repository-state/internal/embedding/tests/SqliteEmbeddingVectorCache.spec.ts
new file mode 100644
index 00000000..f95e174c
--- /dev/null
+++ b/packages/v8/src/modules/repository-state/internal/embedding/tests/SqliteEmbeddingVectorCache.spec.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+
+import { SqliteEmbeddingVectorCache } from "../SqliteEmbeddingVectorCache";
+
+describe("SqliteEmbeddingVectorCache", () => {
+ it("falls back to memory when SQLite exec/get/set fails", () => {
+ const cache = new SqliteEmbeddingVectorCache({
+ exec: () => {
+ throw new Error("SQLITE_ERROR: no such table");
+ },
+ prepare: () => ({
+ get: () => {
+ throw new Error("SQLITE_ERROR");
+ },
+ run: () => {
+ throw new Error("SQLITE_ERROR");
+ },
+ }),
+ } as never);
+
+ expect(cache.get("profile", "hash")).toBeUndefined();
+ expect(() =>
+ cache.set("profile", "hash", [0.1, 0.2, 0.3]),
+ ).not.toThrow();
+ expect(cache.get("profile", "hash")).toEqual([0.1, 0.2, 0.3]);
+ });
+});
diff --git a/packages/v8/src/modules/repository-state/pipeline/ws-indexing-pipeline/WorkspaceIndexingRootFinalizer.ts b/packages/v8/src/modules/repository-state/pipeline/ws-indexing-pipeline/WorkspaceIndexingRootFinalizer.ts
index ba651167..22c0067c 100644
--- a/packages/v8/src/modules/repository-state/pipeline/ws-indexing-pipeline/WorkspaceIndexingRootFinalizer.ts
+++ b/packages/v8/src/modules/repository-state/pipeline/ws-indexing-pipeline/WorkspaceIndexingRootFinalizer.ts
@@ -318,54 +318,89 @@ export class WorkspaceIndexingRootFinalizer {
input.request
.synchronizeEmbeddings
) {
+ // Keep synchronizing until the text-index revision is fully consumed.
+ // A single partial pass used to leave vectorIndex chronically degraded.
+ const maxEmbeddingPasses = 64;
try {
- const embedding =
- await this.dependencies
- .embedding
- .synchronize({
- workspace:
- input.request
- .workspace,
- rootId,
- updatedAt:
- input.request
- .indexedAt,
- ...(input.request
- .abortSignal
- ? {
- abortSignal:
- input
- .request
- .abortSignal,
- }
- : {}),
- });
+ for (
+ let pass = 0;
+ pass < maxEmbeddingPasses;
+ pass += 1
+ ) {
+ if (
+ input.request.abortSignal
+ ?.aborted
+ ) {
+ embeddingStatus =
+ "cancelled";
+ break;
+ }
- embeddingStatus =
- embedding.status;
- embeddingProfileId =
- embedding
- .profile.id;
- initialTextRevision =
- embedding
- .initialTextRevision;
- finalTextRevision =
- embedding
- .finalTextRevision;
- latestTextRevision =
- embedding
- .latestTextRevision;
- embeddedChunks =
- embedding
- .statistics
- .chunksEmbedded;
- vectorsDeleted =
- embedding
- .statistics
- .vectorsDeleted;
+ const embedding =
+ await this.dependencies
+ .embedding
+ .synchronize({
+ workspace:
+ input.request
+ .workspace,
+ rootId,
+ updatedAt:
+ input.request
+ .indexedAt,
+ ...(input.request
+ .abortSignal
+ ? {
+ abortSignal:
+ input
+ .request
+ .abortSignal,
+ }
+ : {}),
+ });
+
+ embeddingStatus =
+ embedding.status;
+ embeddingProfileId =
+ embedding
+ .profile.id;
+ if (
+ initialTextRevision ===
+ undefined
+ ) {
+ initialTextRevision =
+ embedding
+ .initialTextRevision;
+ }
+ finalTextRevision =
+ embedding
+ .finalTextRevision;
+ latestTextRevision =
+ embedding
+ .latestTextRevision;
+ embeddedChunks +=
+ embedding
+ .statistics
+ .chunksEmbedded;
+ vectorsDeleted +=
+ embedding
+ .statistics
+ .vectorsDeleted;
+
+ if (
+ embedding.status !==
+ "partial"
+ ) {
+ break;
+ }
+ }
} catch (
error
) {
+ // Mark partial so publish/UI show degraded vectors instead of
+ // "never configured" (undefined embeddingStatus → unavailable).
+ embeddingStatus =
+ embeddingStatus ??
+ "partial";
warnings.push(
this.warning(
rootId,
diff --git a/packages/v8/src/modules/request-understanding/intent/classifiers/rule/RulePatterns.ts b/packages/v8/src/modules/request-understanding/intent/classifiers/rule/RulePatterns.ts
index a0dd40b3..45d91a71 100644
--- a/packages/v8/src/modules/request-understanding/intent/classifiers/rule/RulePatterns.ts
+++ b/packages/v8/src/modules/request-understanding/intent/classifiers/rule/RulePatterns.ts
@@ -44,9 +44,12 @@ const INTENT_PATTERNS: IntentRule[] = [
confidence: 0.84,
},
{
+ // Diff/PR/change review asks only — bare "code review" as a topic is not
+ // a perform-review intent (those stay questions / diagnose without
+ // structured findings unless the Review button injects host markers).
intent: "review",
pattern:
- /\b(?:review|critique)\b.*\b(?:diff|pr|pull request|patch|commit|code|changes|working[- ]tree|findings?)\b|\b(?:code\s*review|emit_review_finding)\b|\bgenerate\b.*\bpr\s+(?:description|summary)\b/i,
+ /\b(?:review|critique)\b.*\b(?:diff|pr|pull request|patch|commit|changes|working[- ]tree|findings?)\b|\bemit_review_finding\b|\bgenerate\b.*\bpr\s+(?:description|summary)\b/i,
confidence: 0.86,
},
{
diff --git a/packages/v8/src/modules/skills/contracts/input/SkillsSelectInput.ts b/packages/v8/src/modules/skills/contracts/input/SkillsSelectInput.ts
index 669d2edd..b4fe3b60 100644
--- a/packages/v8/src/modules/skills/contracts/input/SkillsSelectInput.ts
+++ b/packages/v8/src/modules/skills/contracts/input/SkillsSelectInput.ts
@@ -68,6 +68,14 @@ export const skillsSelectInputSchema = z
.array(z.string().min(1).max(64))
.max(MAX_REQUIRED_SKILLS)
.default([]),
+ /**
+ * Skill ids that must not be auto-matched for this run.
+ * Required skills still win if explicitly attached.
+ */
+ excludedSkillIds: z
+ .array(z.string().min(1).max(64))
+ .max(20)
+ .default([]),
/**
* When true, omit sizeClass L skills unless alwaysApply / required.
* Engine sets this for compact no_cache windows.
diff --git a/packages/v8/src/modules/skills/pipeline/SkillsPipeline.ts b/packages/v8/src/modules/skills/pipeline/SkillsPipeline.ts
index c25c7b87..488265e4 100644
--- a/packages/v8/src/modules/skills/pipeline/SkillsPipeline.ts
+++ b/packages/v8/src/modules/skills/pipeline/SkillsPipeline.ts
@@ -153,7 +153,22 @@ export class SkillsPipeline {
`${matched.nonMatchedCount} of ${catalog.length} catalog skill(s) did not match this request (path/intent/route/keyword/threshold) and were not considered.`,
);
}
- const merged = mergeSkillCandidates(required.scored, matched.scored);
+ const excluded = new Set(
+ parsed.excludedSkillIds.map((id) => id.trim()).filter(Boolean),
+ );
+ const matchedWithoutExcluded =
+ excluded.size === 0
+ ? matched.scored
+ : matched.scored.filter((entry) => !excluded.has(entry.skill.id));
+ if (
+ excluded.size > 0 &&
+ matchedWithoutExcluded.length < matched.scored.length
+ ) {
+ warnings.push(
+ `Excluded ${matched.scored.length - matchedWithoutExcluded.length} auto-matched skill(s) for this run.`,
+ );
+ }
+ const merged = mergeSkillCandidates(required.scored, matchedWithoutExcluded);
const conflicts = resolveSkillConflicts({ scored: merged });
if (conflicts.conflictsResolved) {
reasonCodes.push("conflicts_resolved");
diff --git a/packages/v8/src/modules/skills/tests/SkillsPipeline.spec.ts b/packages/v8/src/modules/skills/tests/SkillsPipeline.spec.ts
index f69b207d..cd0420ec 100644
--- a/packages/v8/src/modules/skills/tests/SkillsPipeline.spec.ts
+++ b/packages/v8/src/modules/skills/tests/SkillsPipeline.spec.ts
@@ -571,6 +571,45 @@ describe("SkillsPipeline", () => {
).toBe("required");
});
+ it("excludes auto-matched skills listed in excludedSkillIds", async () => {
+ const pipeline = new SkillsPipeline({
+ catalog: new InMemorySkillsCatalog(catalog),
+ });
+
+ const included = await pipeline.select(
+ baseInput({
+ route: "execute",
+ query: "Fix the null check",
+ evidence: {
+ primaryIntent: "bugfix",
+ secondaryIntents: [],
+ },
+ }),
+ );
+ expect(included.instructions.map((block) => block.id)).toContain(
+ "bugfix-localize",
+ );
+
+ const excluded = await pipeline.select(
+ baseInput({
+ route: "execute",
+ query: "Fix the null check",
+ evidence: {
+ primaryIntent: "bugfix",
+ secondaryIntents: [],
+ },
+ excludedSkillIds: ["bugfix-localize"],
+ }),
+ );
+
+ expect(excluded.instructions.map((block) => block.id)).not.toContain(
+ "bugfix-localize",
+ );
+ expect(
+ excluded.warnings.some((warning) => warning.includes("Excluded")),
+ ).toBe(true);
+ });
+
it("reports not_found omissions for missing required skills", async () => {
const pipeline = new SkillsPipeline({
catalog: new InMemorySkillsCatalog(catalog),
diff --git a/packages/v8/vitest.config.ts b/packages/v8/vitest.config.ts
index 9dafabaf..3e277ab2 100644
--- a/packages/v8/vitest.config.ts
+++ b/packages/v8/vitest.config.ts
@@ -36,6 +36,7 @@ export default defineConfig({
'src/modules/repository-state/internal/chunking/tests/CodeCollapse.spec.ts',
'src/modules/repository-state/internal/text-index/tests/TrigramFts.spec.ts',
'src/modules/repository-state/internal/embedding/tests/EmbeddingVectorCache.spec.ts',
+ 'src/modules/repository-state/internal/embedding/tests/SqliteEmbeddingVectorCache.spec.ts',
'src/modules/repository-state/pipeline/ws-indexing-pipeline/tests/FileProcessorStatFreshness.spec.ts',
'src/modules/model-gateway/tests/OpenAiCompatibleRetry.spec.ts',
'src/modules/model-gateway/tests/AnthropicLlmPort.spec.ts',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 70e75db7..f82c1f2d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
'@types/node':
specifier: ^20.14.0
version: 20.19.43
+ jsdom:
+ specifier: ^26.1.0
+ version: 26.1.0
knip:
specifier: ^6.21.0
version: 6.24.0
@@ -28,7 +31,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
apps/acp:
dependencies:
@@ -81,7 +84,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
optionalDependencies:
'@lancedb/lancedb':
specifier: 0.33.0
@@ -180,6 +183,12 @@ importers:
specifier: ^4.0.1
version: 4.0.1
devDependencies:
+ '@testing-library/react':
+ specifier: ^16.3.3
+ version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@testing-library/user-event':
+ specifier: ^14.6.7
+ version: 14.6.7(@testing-library/dom@10.4.2)
'@types/react':
specifier: ^18.3.12
version: 18.3.31
@@ -189,6 +198,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.7.0(vite@5.4.21(@types/node@20.19.43))
+ jsdom:
+ specifier: ^26.1.0
+ version: 26.1.0
typescript:
specifier: ^5.5.2
version: 5.9.3
@@ -216,7 +228,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
packages/host:
dependencies:
@@ -247,7 +259,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
optionalDependencies:
'@lancedb/lancedb':
specifier: 0.33.0
@@ -282,7 +294,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
packages/mcp/web:
dependencies:
@@ -301,7 +313,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
packages/sdk:
dependencies:
@@ -320,7 +332,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
packages/search-kit:
dependencies:
@@ -336,7 +348,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
packages/v8:
dependencies:
@@ -367,7 +379,7 @@ importers:
version: 12.11.1
vitest:
specifier: ^3.2.7
- version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)
+ version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0)
tests: {}
@@ -382,6 +394,9 @@ importers:
packages:
+ '@asamuzakjp/css-color@3.2.0':
+ resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
'@azu/format-text@1.0.2':
resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==}
@@ -503,6 +518,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
@@ -515,6 +534,34 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
'@dependents/detective-less@5.0.3':
resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==}
engines: {node: '>=18'}
@@ -1472,6 +1519,31 @@ packages:
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
+ '@testing-library/dom@10.4.2':
+ resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==}
+ engines: {node: '>=18'}
+
+ '@testing-library/react@16.3.3':
+ resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@testing-library/user-event@14.6.7':
+ resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@textlint/ast-node-types@15.7.1':
resolution: {integrity: sha512-Wii5UgUKFEh9Uv6wbq1zr4/Kf+dtjiUuzPrrXzKp8H+ifkvKNzi23V4Nz+6wVyHQn5T28AFuc8VH8OtzvGYecA==}
@@ -1506,6 +1578,9 @@ packages:
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1757,6 +1832,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -1773,6 +1852,9 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
array-back@3.1.0:
resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==}
engines: {node: '>=6'}
@@ -2007,9 +2089,17 @@ packages:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
+ cssstyle@4.6.0:
+ resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+ engines: {node: '>=18'}
+
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -2019,6 +2109,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@@ -2126,6 +2219,9 @@ packages:
digest-fetch@1.3.0:
resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
@@ -2460,6 +2556,10 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -2561,6 +2661,9 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-regexp@1.0.0:
resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==}
engines: {node: '>=0.10.0'}
@@ -2605,6 +2708,15 @@ packages:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
hasBin: true
+ jsdom@26.1.0:
+ resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -2717,6 +2829,10 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
madge@8.0.0:
resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==}
engines: {node: '>=18'}
@@ -3008,6 +3124,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ nwsapi@2.2.27:
+ resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -3163,6 +3282,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
pretty-ms@7.0.1:
resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==}
engines: {node: '>=10'}
@@ -3185,6 +3308,10 @@ packages:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
qs@6.15.3:
resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==}
engines: {node: '>=0.6'}
@@ -3207,6 +3334,9 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react-markdown@10.1.0:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies:
@@ -3294,6 +3424,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
+ rrweb-cssom@0.8.0:
+ resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
run-applescript@7.1.0:
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
engines: {node: '>=18'}
@@ -3316,6 +3449,10 @@ packages:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -3500,6 +3637,9 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
table-layout@4.1.1:
resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
engines: {node: '>=12.17'}
@@ -3556,6 +3696,13 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tldts-core@6.1.86:
+ resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
+
+ tldts@6.1.86:
+ resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
+ hasBin: true
+
tmp@0.2.7:
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
engines: {node: '>=14.14'}
@@ -3564,9 +3711,17 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ tough-cookie@5.1.2:
+ resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ tr46@5.1.1:
+ resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+ engines: {node: '>=18'}
+
tree-sitter-wasms@0.1.13:
resolution: {integrity: sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==}
@@ -3767,6 +3922,10 @@ packages:
jsdom:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
walk-up-path@4.0.0:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
@@ -3792,6 +3951,10 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
@@ -3801,6 +3964,10 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
+ whatwg-url@14.2.0:
+ resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
+ engines: {node: '>=18'}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -3826,10 +3993,26 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ ws@8.21.3:
+ resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
xml-naming@0.3.0:
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
engines: {node: '>=16.0.0'}
@@ -3842,6 +4025,9 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -3875,6 +4061,14 @@ packages:
snapshots:
+ '@asamuzakjp/css-color@3.2.0':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 10.4.3
+
'@azu/format-text@1.0.2': {}
'@azu/style-format@1.0.1':
@@ -4052,6 +4246,8 @@ snapshots:
'@babel/core': 7.29.7
'@babel/helper-plugin-utils': 7.29.7
+ '@babel/runtime@7.29.7': {}
+
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
@@ -4075,6 +4271,26 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
'@dependents/detective-less@5.0.3':
dependencies:
gonzales-pe: 4.3.0
@@ -4765,6 +4981,31 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@testing-library/dom@10.4.2':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@testing-library/dom': 10.4.2
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.31
+ '@types/react-dom': 18.3.7(@types/react@18.3.31)
+
+ '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)':
+ dependencies:
+ '@testing-library/dom': 10.4.2
+
'@textlint/ast-node-types@15.7.1': {}
'@textlint/linter-formatter@15.7.1':
@@ -4814,6 +5055,8 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/aria-query@5.0.4': {}
+
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.7
@@ -5147,6 +5390,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
+ ansi-styles@5.2.0: {}
+
any-promise@1.3.0: {}
anynum@1.0.1: {}
@@ -5168,6 +5413,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
array-back@3.1.0:
optional: true
@@ -5406,12 +5655,24 @@ snapshots:
css-what@6.2.2: {}
+ cssstyle@4.6.0:
+ dependencies:
+ '@asamuzakjp/css-color': 3.2.0
+ rrweb-cssom: 0.8.0
+
csstype@3.2.3: {}
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
debug@4.4.3:
dependencies:
ms: 2.1.3
+ decimal.js@10.6.0: {}
+
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@@ -5536,6 +5797,8 @@ snapshots:
md5: 2.3.0
optional: true
+ dom-accessibility-api@0.5.16: {}
+
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
@@ -5947,6 +6210,10 @@ snapshots:
dependencies:
lru-cache: 10.4.3
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
html-url-attributes@3.0.1: {}
htmlparser2@10.1.0:
@@ -6034,6 +6301,8 @@ snapshots:
is-plain-obj@4.1.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-regexp@1.0.0: {}
is-unicode-supported@0.1.0: {}
@@ -6066,6 +6335,33 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsdom@26.1.0:
+ dependencies:
+ cssstyle: 4.6.0
+ data-urls: 5.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.27
+ parse5: 7.3.0
+ rrweb-cssom: 0.8.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 5.1.2
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+ ws: 8.21.3
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jsesc@3.1.0: {}
json-bignum@0.0.3:
@@ -6187,6 +6483,8 @@ snapshots:
dependencies:
yallist: 4.0.0
+ lz-string@1.5.0: {}
+
madge@8.0.0(typescript@5.9.3):
dependencies:
chalk: 4.1.2
@@ -6694,6 +6992,8 @@ snapshots:
dependencies:
boolbase: 1.0.0
+ nwsapi@2.2.27: {}
+
object-inspect@1.13.4: {}
object-keys@1.1.1:
@@ -6947,6 +7247,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
pretty-ms@7.0.1:
dependencies:
parse-ms: 2.1.0
@@ -6977,6 +7283,8 @@ snapshots:
punycode.js@2.3.1: {}
+ punycode@2.3.1: {}
+
qs@6.15.3:
dependencies:
es-define-property: 1.0.1
@@ -7008,6 +7316,8 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-is@17.0.2: {}
+
react-markdown@10.1.0(@types/react@18.3.31)(react@18.3.1):
dependencies:
'@types/hast': 3.0.5
@@ -7161,6 +7471,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.62.2
fsevents: 2.3.3
+ rrweb-cssom@0.8.0: {}
+
run-applescript@7.1.0: {}
run-parallel@1.2.0:
@@ -7178,6 +7490,10 @@ snapshots:
sax@1.6.0: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -7398,6 +7714,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
table-layout@4.1.1:
dependencies:
array-back: 6.2.3
@@ -7463,15 +7781,29 @@ snapshots:
tinyspy@4.0.4: {}
+ tldts-core@6.1.86: {}
+
+ tldts@6.1.86:
+ dependencies:
+ tldts-core: 6.1.86
+
tmp@0.2.7: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
+ tough-cookie@5.1.2:
+ dependencies:
+ tldts: 6.1.86
+
tr46@0.0.3:
optional: true
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
tree-sitter-wasms@0.1.13:
optional: true
@@ -7631,7 +7963,7 @@ snapshots:
'@types/node': 20.19.43
fsevents: 2.3.3
- vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43):
+ vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(jsdom@26.1.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.7
@@ -7659,6 +7991,7 @@ snapshots:
optionalDependencies:
'@types/debug': 4.1.13
'@types/node': 20.19.43
+ jsdom: 26.1.0
transitivePeerDependencies:
- less
- lightningcss
@@ -7670,6 +8003,10 @@ snapshots:
- supports-color
- terser
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
walk-up-path@4.0.0: {}
walkdir@0.4.1: {}
@@ -7690,12 +8027,19 @@ snapshots:
webidl-conversions@3.0.1:
optional: true
+ webidl-conversions@7.0.0: {}
+
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@4.0.0: {}
+ whatwg-url@14.2.0:
+ dependencies:
+ tr46: 5.1.1
+ webidl-conversions: 7.0.0
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -7720,10 +8064,14 @@ snapshots:
wrappy@1.0.2: {}
+ ws@8.21.3: {}
+
wsl-utils@0.1.0:
dependencies:
is-wsl: 3.1.1
+ xml-name-validator@5.0.0: {}
+
xml-naming@0.3.0: {}
xml2js@0.5.0:
@@ -7733,6 +8081,8 @@ snapshots:
xmlbuilder@11.0.1: {}
+ xmlchars@2.2.0: {}
+
yallist@3.1.1: {}
yallist@4.0.0: {}