diff --git a/apps/acp/package.json b/apps/acp/package.json
index 6b58e605..0b8fc0eb 100644
--- a/apps/acp/package.json
+++ b/apps/acp/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/acp",
- "version": "2.9.57",
+ "version": "2.9.58",
"description": "Mitii ACP-lite stdio bridge (Phase 3). Decision Policy remains authority; V8 does not import ACP.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 50c08350..7b63f04f 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/cli",
- "version": "2.9.57",
+ "version": "2.9.58",
"description": "Mitii headless CLI over @mitii/sdk. Phase 0: --origin/--autonomy/--agent for CI automation.",
"license": "AGPL-3.0-or-later",
"publishConfig": {
diff --git a/apps/daemon/package.json b/apps/daemon/package.json
index 5938f960..1a683e23 100644
--- a/apps/daemon/package.json
+++ b/apps/daemon/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/daemon",
- "version": "2.9.57",
+ "version": "2.9.58",
"description": "Mitii automation daemon process entry (Phase 1). Long-lived schedule runner.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/apps/vscode/package.json b/apps/vscode/package.json
index 8ea88bf9..80fbd99a 100644
--- a/apps/vscode/package.json
+++ b/apps/vscode/package.json
@@ -2,7 +2,7 @@
"name": "mitii-ai-agent",
"displayName": "Mitii AI Agent",
"description": "Local-first VS Code AI coding agent with repository-aware context and controlled execution",
- "version": "2.9.57",
+ "version": "2.9.58",
"publisher": "mitii",
"license": "AGPL-3.0-or-later",
"icon": "media/mitii-logo.png",
diff --git a/package.json b/package.json
index 70cd6683..b2593b94 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.58",
"private": true,
"license": "AGPL-3.0-or-later",
"author": {
diff --git a/packages/automation/package.json b/packages/automation/package.json
index 1d31bbb8..cd69f075 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.58",
"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/host/package.json b/packages/host/package.json
index 5ce1fc24..c952cbca 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.58",
"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/mcp/package.json b/packages/mcp/package.json
index 31907166..e92a5af9 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.58",
"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..1365d14f 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.58",
"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..69cbeb8d 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.58",
"description": "Host-neutral Mitii programmatic API over @mitii/v8 Agent Engine.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/packages/search-kit/package.json b/packages/search-kit/package.json
index 65f9d2ea..1e4a58d9 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.58",
"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..e640ee45 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.58",
"description": "Host-neutral Mitii V8 agent runtime (modules + engine).",
"license": "AGPL-3.0-or-later",
"type": "module",
From 86d74e4125ad2db84521c1e40b34baafa98cffda Mon Sep 17 00:00:00 2001
From: codewithshinde
Date: Thu, 17 Sep 2026 21:40:19 -0500
Subject: [PATCH 2/9] feat(vscode): settings panel overhaul, live sandbox
process, and routing improvements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
VS Code extension:
- Rework settings panel with round-trip persistence (sidebar ↔ webview)
- Add live sandbox process support (liveSandboxProcess.ts + tests)
- Update NumberField, SliderField, ComposerControls, and related UI components
- Add vitest infrastructure for webview-ui (vitest.config.ts, vscode mock)
- Add SETTINGS_COVERAGE.md and settings harness test helper
- Clean up redundant activation events in package.json
Agent engine / decision policy:
- Refine ResolveRoute and BuildToolGrant actions
- Update RulePatterns classifier
- Add DecisionPolicyPipeline spec cases
Skills pipeline:
- Extend SkillsSelectInput contract and pipeline logic
- Add SkillsPipeline spec coverage
SDK / host:
- Add new contract fields (packages/sdk)
- Update createHostLlmPorts spec and testProviderConnection
Misc:
- Update CLI help text and READMEs
- pnpm-lock.yaml sync
---
README.md | 2 +-
apps/acp/package.json | 2 +-
apps/cli/README.md | 2 +-
apps/cli/package.json | 2 +-
apps/cli/src/help.ts | 2 +-
apps/daemon/package.json | 2 +-
apps/vscode/README.md | 2 +-
apps/vscode/package.json | 34 +-
apps/vscode/src/hostAsk.ts | 5 +
apps/vscode/src/liveSandboxProcess.ts | 85 ++++
apps/vscode/src/ports.ts | 32 +-
apps/vscode/src/protocol.ts | 23 +-
apps/vscode/src/settingsFields.ts | 18 +-
apps/vscode/src/sidebar.ts | 109 +++++-
apps/vscode/tests/SETTINGS_COVERAGE.md | 29 ++
apps/vscode/tests/helpers/settingsHarness.ts | 66 ++++
apps/vscode/tests/liveSandboxProcess.test.ts | 96 +++++
apps/vscode/tests/settingsFields.test.ts | 76 +---
apps/vscode/tests/settingsRoundTrip.test.ts | 105 +++++
.../tests/sidebarSettingsPersistence.test.ts | 62 +--
apps/vscode/webview-ui/package.json | 6 +-
apps/vscode/webview-ui/src/App.tsx | 88 ++++-
.../src/components/ComposerControls.tsx | 2 +-
.../src/components/IndexingStatusBar.tsx | 14 +-
.../webview-ui/src/components/MessageList.tsx | 2 +-
.../webview-ui/src/components/NumberField.tsx | 12 +-
.../webview-ui/src/components/ReviewPanel.tsx | 2 +-
.../src/components/SettingsPanel.tsx | 80 ++--
.../webview-ui/src/components/SliderField.tsx | 1 +
.../src/components/WorkingTreeReviewBar.tsx | 23 +-
apps/vscode/webview-ui/src/protocol.ts | 23 +-
apps/vscode/webview-ui/src/styles.css | 24 +-
.../vscode/webview-ui/tests/settings.test.tsx | 254 ++++++++++++
apps/vscode/webview-ui/tests/vscode.ts | 2 +
apps/vscode/webview-ui/vitest.config.ts | 20 +
package.json | 8 +-
packages/automation/package.json | 2 +-
packages/host/package.json | 2 +-
.../src/config/createHostLlmPorts.spec.ts | 21 +
.../host/src/config/testProviderConnection.ts | 5 +-
packages/mcp/package.json | 2 +-
packages/mcp/web/package.json | 2 +-
packages/sdk/package.json | 2 +-
packages/sdk/src/contracts.ts | 4 +
packages/search-kit/package.json | 2 +-
packages/v8/package.json | 2 +-
.../contracts/input/AgentEngineInput.ts | 5 +
.../agent-engine/pipeline/executeStart.ts | 2 +
.../pipeline/executeStartEnrichment.ts | 1 +
.../pipeline/executeToolSupport.ts | 2 +
.../pipeline/modelLoopToolPhase.ts | 3 +
.../agent-engine/pipeline/modelToolLoop.ts | 2 +
.../pipeline/verificationFinish.ts | 2 +
.../decision-policy/actions/BuildToolGrant.ts | 7 +-
.../decision-policy/actions/ResolveRoute.ts | 28 +-
.../modules/decision-policy/actions/index.ts | 2 +-
.../tests/DecisionPolicyPipeline.spec.ts | 44 +++
.../tests/fixtures/goldenCases.ts | 8 +-
.../intent/classifiers/rule/RulePatterns.ts | 5 +-
.../contracts/input/SkillsSelectInput.ts | 8 +
.../modules/skills/pipeline/SkillsPipeline.ts | 17 +-
.../skills/tests/SkillsPipeline.spec.ts | 39 ++
pnpm-lock.yaml | 370 +++++++++++++++++-
63 files changed, 1619 insertions(+), 285 deletions(-)
create mode 100644 apps/vscode/src/liveSandboxProcess.ts
create mode 100644 apps/vscode/tests/SETTINGS_COVERAGE.md
create mode 100644 apps/vscode/tests/helpers/settingsHarness.ts
create mode 100644 apps/vscode/tests/liveSandboxProcess.test.ts
create mode 100644 apps/vscode/tests/settingsRoundTrip.test.ts
create mode 100644 apps/vscode/webview-ui/tests/settings.test.tsx
create mode 100644 apps/vscode/webview-ui/tests/vscode.ts
create mode 100644 apps/vscode/webview-ui/vitest.config.ts
diff --git a/README.md b/README.md
index 9369045f..e786f70d 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+
diff --git a/apps/acp/package.json b/apps/acp/package.json
index 0b8fc0eb..54e48711 100644
--- a/apps/acp/package.json
+++ b/apps/acp/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/acp",
- "version": "2.9.58",
+ "version": "2.9.59",
"description": "Mitii ACP-lite stdio bridge (Phase 3). Decision Policy remains authority; V8 does not import ACP.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/apps/cli/README.md b/apps/cli/README.md
index de2f3fe7..e2192088 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -71,7 +71,7 @@ mitii export-session "Summarize this repo" --out session.json --echo
| `run --auto ""` | Unattended CI run (agent + apply autonomy; no prompts) |
| `session` | Interactive prompt loop with MITII banner |
| `index` | Full workspace index + publish repository state |
-| `review` | Deterministic review prep / SARIF (`--preview`, `--from`/`--to`, `--commit`, `--format`, `--output`). For LLM findings use `mitii ask … --skill code-review-and-quality` or VS Code **Review** mode |
+| `review` | Deterministic review prep / SARIF (`--preview`, `--from`/`--to`, `--commit`, `--format`, `--output`). For LLM findings use `mitii ask … --skill code-review-and-quality` (recipe/skill) or the VS Code **Review** button for working-tree changes |
| `status` | Show latest persisted repository state |
| `export-session` | Run ask and write secret-free JSON export |
| `connect` | Bridge Mitii into Telegram, Discord, or Slack |
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 7b63f04f..c0c65387 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/cli",
- "version": "2.9.58",
+ "version": "2.9.59",
"description": "Mitii headless CLI over @mitii/sdk. Phase 0: --origin/--autonomy/--agent for CI automation.",
"license": "AGPL-3.0-or-later",
"publishConfig": {
diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts
index 7a9d97ea..f4f7e55b 100644
--- a/apps/cli/src/help.ts
+++ b/apps/cli/src/help.ts
@@ -46,7 +46,7 @@ Commands:
--format json|sarif Output shape (default json)
--output Write result to file
--effort low|medium|high Prep effort band
- Full LLM review: VS Code Review mode, or:
+ Full LLM review: VS Code Review button (git changes), or:
mitii ask "review these changes" --mode ask --skill code-review-and-quality
status Show latest persisted repository state
export-session Run ask and write secret-free JSON export
diff --git a/apps/daemon/package.json b/apps/daemon/package.json
index 1a683e23..abe3274b 100644
--- a/apps/daemon/package.json
+++ b/apps/daemon/package.json
@@ -1,6 +1,6 @@
{
"name": "@mitii/daemon",
- "version": "2.9.58",
+ "version": "2.9.59",
"description": "Mitii automation daemon process entry (Phase 1). Long-lived schedule runner.",
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/apps/vscode/README.md b/apps/vscode/README.md
index 9466c9eb..992f9782 100644
--- a/apps/vscode/README.md
+++ b/apps/vscode/README.md
@@ -45,7 +45,7 @@ For cloud providers, run **Mitii: Set Provider API Key** (stored in VS Code Secr
| Command | Purpose |
|---|---|
| **Mitii: Open Chat** | Open the sidebar |
-| **Mitii: Review Working Tree Changes** | Switch to Review mode and run a structured review |
+| **Mitii: Review Working Tree Changes** | Run a structured review of git changes (Review button). Optional Code Review button when enabled under Settings → Features |
| **Mitii: Index Workspace** | Rebuild repository index |
| **Mitii: Show Settings** | Provider, index, MCP, workspace |
| **Mitii: Toggle Autocomplete** | Enable or disable FIM inline suggestions |
diff --git a/apps/vscode/package.json b/apps/vscode/package.json
index 80fbd99a..8af68120 100644
--- a/apps/vscode/package.json
+++ b/apps/vscode/package.json
@@ -2,7 +2,7 @@
"name": "mitii-ai-agent",
"displayName": "Mitii AI Agent",
"description": "Local-first VS Code AI coding agent with repository-aware context and controlled execution",
- "version": "2.9.58",
+ "version": "2.9.59",
"publisher": "mitii",
"license": "AGPL-3.0-or-later",
"icon": "media/mitii-logo.png",
@@ -1028,6 +1028,11 @@
"default": true,
"description": "Show live reasoning / thinking activity in the Mitii chat UI"
},
+ "mitii.ui.features.codeReviewButton": {
+ "type": "boolean",
+ "default": false,
+ "description": "Show the Code Review button on the working-tree review bar (in addition to Review). Off by default; enable under Settings → Features."
+ },
"mitii.ui.reasoningPreviewMaxChars": {
"type": "number",
"default": 8000,
@@ -1365,6 +1370,33 @@
}
}
}
+ },
+ "mitii.tokenBudget.visiblePlanMinUsableRatio": {
+ "type": "number",
+ "default": 0.35,
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Visible-plan affordability threshold as a fraction of the context window."
+ },
+ "mitii.tokenBudget.changeImpactMinUsableRatio": {
+ "type": "number",
+ "default": 0.35,
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Change-impact affordability threshold as a fraction of the context window."
+ },
+ "mitii.tokenBudget.maxPatchesPerCallCap": {
+ "type": "number",
+ "default": 96,
+ "minimum": 1,
+ "description": "Maximum patches allowed in one mutation call."
+ },
+ "mitii.loopPolicy.maxStructuredReviewRecoveries": {
+ "type": "number",
+ "default": 2,
+ "minimum": 0,
+ "maximum": 8,
+ "description": "Recovery attempts when a structured review ends without findings (0 disables)."
}
}
}
diff --git a/apps/vscode/src/hostAsk.ts b/apps/vscode/src/hostAsk.ts
index 2ee17dc5..50b9aa93 100644
--- a/apps/vscode/src/hostAsk.ts
+++ b/apps/vscode/src/hostAsk.ts
@@ -166,6 +166,8 @@ export async function runAskInOutputChannel(options: {
approvalMode?: string;
pinnedPaths?: string[];
requiredSkillIds?: string[];
+ /** Skills that must not be auto-matched for this run. */
+ excludedSkillIds?: string[];
/** Attached MCP server ids for this turn (@mcp: / host pin). */
requiredMcpServerIds?: string[];
workspaceId?: string;
@@ -475,6 +477,9 @@ export async function runAskInOutputChannel(options: {
...(options.requiredSkillIds && options.requiredSkillIds.length > 0
? { requiredSkillIds: [...options.requiredSkillIds] }
: {}),
+ ...(options.excludedSkillIds && options.excludedSkillIds.length > 0
+ ? { excludedSkillIds: [...options.excludedSkillIds] }
+ : {}),
...(options.requiredMcpServerIds &&
options.requiredMcpServerIds.length > 0
? { requiredMcpServerIds: [...options.requiredMcpServerIds] }
diff --git a/apps/vscode/src/liveSandboxProcess.ts b/apps/vscode/src/liveSandboxProcess.ts
new file mode 100644
index 00000000..e0e095f4
--- /dev/null
+++ b/apps/vscode/src/liveSandboxProcess.ts
@@ -0,0 +1,85 @@
+import type * as vscode from 'vscode';
+import type { ProcessPort, ProcessExecRequest } from '@mitii/v8';
+import {
+ createSandboxedProcessPort,
+ resolveSandboxPolicy,
+ resolveSandboxSettingsFromPreset,
+ type SandboxBackend,
+ type SandboxNetworkMode,
+} from '@mitii/host';
+
+type ResolvedSandboxSettings = {
+ enabled: boolean;
+ network: SandboxNetworkMode;
+};
+
+function readSandboxSettings(vs: typeof vscode): ResolvedSandboxSettings {
+ const cfg = vs.workspace.getConfiguration('mitii');
+ const configured = (key: string): boolean => {
+ const value = cfg.inspect(key);
+ return (
+ value?.globalValue !== undefined ||
+ value?.workspaceValue !== undefined ||
+ value?.workspaceFolderValue !== undefined
+ );
+ };
+ return resolveSandboxSettingsFromPreset({
+ approvalMode: cfg.get('safety.approvalMode') ?? 'guided',
+ ...(configured('safety.sandbox.enabled')
+ ? { enabled: cfg.get('safety.sandbox.enabled') === true }
+ : {}),
+ ...(configured('safety.sandbox.network')
+ ? { network: cfg.get('safety.sandbox.network') ?? 'deny' }
+ : {}),
+ });
+}
+
+function settingsKey(settings: ResolvedSandboxSettings): string {
+ return `${settings.enabled ? 1 : 0}:${settings.network}`;
+}
+
+/**
+ * Process port that refreshes access policy when settings change, without
+ * rebuilding a wrapper on every exec.
+ *
+ * Once sandbox has been enabled for this port, it cannot be disabled through
+ * live settings (fail closed). Network may still tighten/loosen with approval
+ * mode so Guided ↔ Full access keeps working mid-session.
+ */
+export function createLiveSandboxedProcessPort(
+ vs: typeof vscode,
+ inner: ProcessPort,
+ workspaceRoot: string,
+ backend: SandboxBackend,
+): ProcessPort {
+ let cachedKey: string | undefined;
+ let cachedPort: ProcessPort | undefined;
+ let sandboxLatchedOn = false;
+
+ const resolvePort = (): ProcessPort => {
+ const raw = readSandboxSettings(vs);
+ const settings: ResolvedSandboxSettings = {
+ // Latch: after sandbox has run enabled, ignore live disable.
+ enabled: raw.enabled || sandboxLatchedOn,
+ network: raw.network,
+ };
+ if (settings.enabled) sandboxLatchedOn = true;
+
+ const key = settingsKey(settings);
+ if (cachedPort && cachedKey === key) return cachedPort;
+
+ cachedKey = key;
+ cachedPort = createSandboxedProcessPort(
+ inner,
+ resolveSandboxPolicy({ ...settings, workspaceRoot }),
+ backend,
+ );
+ return cachedPort;
+ };
+
+ return {
+ execFile(request: ProcessExecRequest) {
+ return resolvePort().execFile(request);
+ },
+ };
+}
diff --git a/apps/vscode/src/ports.ts b/apps/vscode/src/ports.ts
index 75812738..d259c6b3 100644
--- a/apps/vscode/src/ports.ts
+++ b/apps/vscode/src/ports.ts
@@ -26,19 +26,17 @@ import {
createHostNetworkPort,
createHostRepositoryGraphPort,
createOptionalSearchPort,
- createSandboxedProcessPort,
createWorkspaceCheckpointStore,
createWorkspaceKnowledgeGraph,
createWorkspaceVerificationStore,
detectSandboxBackend,
resolveMemoryEmbeddingPort,
- resolveSandboxPolicy,
- resolveSandboxSettingsFromPreset,
resolveProviderApiKey,
type SandboxBackendPrefer,
} from '@mitii/host';
import type * as vscode from 'vscode';
+import { createLiveSandboxedProcessPort } from './liveSandboxProcess.js';
import { VscodeDiagnosticsPort } from './diagnosticsPort.js';
import { getSharedMcpManager } from './mcp/manager.js';
import { defaultMcpSettings, readMcpSettings } from './mcpConfig.js';
@@ -280,25 +278,6 @@ export async function createVscodeClient(
env: searchEnv,
});
const cfg = vs.workspace.getConfiguration('mitii');
- const sandboxInspectEnabled = cfg.inspect('safety.sandbox.enabled');
- const sandboxInspectNetwork = cfg.inspect('safety.sandbox.network');
- const sandboxEnabledUnset =
- sandboxInspectEnabled?.globalValue === undefined &&
- sandboxInspectEnabled?.workspaceValue === undefined &&
- sandboxInspectEnabled?.workspaceFolderValue === undefined;
- const sandboxNetworkUnset =
- sandboxInspectNetwork?.globalValue === undefined &&
- sandboxInspectNetwork?.workspaceValue === undefined &&
- sandboxInspectNetwork?.workspaceFolderValue === undefined;
- const sandboxResolved = resolveSandboxSettingsFromPreset({
- approvalMode: cfg.get('safety.approvalMode') ?? 'guided',
- ...(sandboxEnabledUnset
- ? {}
- : { enabled: cfg.get('safety.sandbox.enabled') === true }),
- ...(sandboxNetworkUnset
- ? {}
- : { network: cfg.get('safety.sandbox.network') ?? 'deny' }),
- });
const sandboxBackendRaw = cfg.get('safety.sandbox.backend') ?? 'auto';
const sandboxPrefer: SandboxBackendPrefer =
sandboxBackendRaw === 'docker' ||
@@ -314,13 +293,10 @@ export async function createVscodeClient(
? new ToolRuntimePipeline(
{
fileSystem,
- process: createSandboxedProcessPort(
+ process: createLiveSandboxedProcessPort(
+ vs,
new NodeProcessAdapter(),
- resolveSandboxPolicy({
- enabled: sandboxResolved.enabled,
- network: sandboxResolved.network,
- workspaceRoot,
- }),
+ workspaceRoot,
detectSandboxBackend({ prefer: sandboxPrefer }),
),
network,
diff --git a/apps/vscode/src/protocol.ts b/apps/vscode/src/protocol.ts
index 58bae2d4..2f46110c 100644
--- a/apps/vscode/src/protocol.ts
+++ b/apps/vscode/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/src/settingsFields.ts b/apps/vscode/src/settingsFields.ts
index ac83978b..30362866 100644
--- a/apps/vscode/src/settingsFields.ts
+++ b/apps/vscode/src/settingsFields.ts
@@ -71,6 +71,7 @@ export const SETTINGS_NAV_ITEMS: readonly {
{ id: 'workspace', label: 'Workspace' },
{ id: 'modes', label: 'Modes' },
{ id: 'context', label: 'Context' },
+ { id: 'features', label: 'Features' },
{ id: 'integrations', label: 'MCP' },
{ id: 'debug', label: 'Developer' },
];
@@ -117,6 +118,7 @@ export type SettingsPage =
| 'workspace'
| 'modes'
| 'context'
+ | 'features'
| 'mcp'
| 'developer';
@@ -246,9 +248,13 @@ export function applyUiPatch(
base: UiSettingsSnapshot,
patch: UiSettingsPatch,
): UiSettingsSnapshot {
+ const { features: _features, ...restPatch } = patch;
return {
...base,
- ...patch,
+ ...restPatch,
+ features: patch.features
+ ? { ...base.features, ...patch.features }
+ : base.features,
contextToggles: patch.contextToggles
? { ...base.contextToggles, ...patch.contextToggles }
: base.contextToggles,
@@ -866,6 +872,16 @@ export const SETTINGS_FIELDS: readonly SettingsFieldSpec[] = [
reflect: 'raw',
sample: false,
},
+ {
+ id: 'ui.features.codeReviewButton',
+ page: 'features',
+ tab: 'features',
+ setting: 'ui.features.codeReviewButton',
+ label: 'Show Code Review button',
+ kind: 'boolean',
+ reflect: 'raw',
+ sample: true,
+ },
{
id: 'ui.reasoningPreviewMaxChars',
page: 'modes',
diff --git a/apps/vscode/src/sidebar.ts b/apps/vscode/src/sidebar.ts
index 491f1bed..f3b3ff48 100644
--- a/apps/vscode/src/sidebar.ts
+++ b/apps/vscode/src/sidebar.ts
@@ -418,20 +418,33 @@ function needsFullIndexRefresh(index: IndexStatusSnapshot): boolean {
return false;
}
-const REVIEW_HOST_PREFIX =
- 'Review every selected file in the current git changes, including both staged and unstaged patches. Start by calling read_git_status with includeDiff=true. You MUST call emit_review_finding at least once before finishing — once per high-signal issue with path, content, existingCode, severity, and category. If there are no material issues, emit a single low/info finding that says so. Prose-only analysis is not a valid review. Do not digress into filename-casing rabbit holes; account for every selected file while preferring high-signal findings.';
+/** Quick git-diff scan — findings only, not multi-axis code review. */
+const REVIEW_CHANGES_HOST_PREFIX =
+ 'Scan the selected files in the current git changes (staged and unstaged). Start by calling read_git_status with includeDiff=true. Report only material bugs, regressions, and security issues introduced by the diff. You MUST call emit_review_finding at least once before finishing — once per high-signal issue with path, content, existingCode, severity, and category. If there are no material issues, emit a single low/info finding that says so. Prose-only analysis is not valid. Do NOT perform a multi-axis code-quality review (architecture, readability style nits, test-coverage coaching, or merge-readiness playbooks). Do NOT follow code-review-and-quality instructions even if a skill is present.';
+
+/** Thorough code review of the same git changes. */
+const REVIEW_CODE_HOST_PREFIX =
+ 'Perform a thorough code review of every selected file in the current git changes, including both staged and unstaged patches. Start by calling read_git_status with includeDiff=true. Assess correctness, readability, architecture, tests, and operational risk. You MUST call emit_review_finding at least once before finishing — once per high-signal issue with path, content, existingCode, severity, and category. If there are no material issues, emit a single low/info finding that says so. Prose-only analysis is not a valid review. Prefer high-signal findings over nits.';
/** Avoid stacking identical review instructions from UI + host. */
-function buildReviewLlmPrompt(userPrompt: string): string {
+function buildReviewLlmPrompt(
+ userPrompt: string,
+ kind: 'changes' | 'code' = 'changes',
+): string {
+ const prefix =
+ kind === 'code' ? REVIEW_CODE_HOST_PREFIX : REVIEW_CHANGES_HOST_PREFIX;
const trimmed = userPrompt.trim();
- if (!trimmed) return REVIEW_HOST_PREFIX;
+ if (!trimmed) return prefix;
+ // Already host-built (e.g. resume / stacked) — keep as-is.
if (
- /\bemit_review_finding\b/i.test(trimmed) ||
- /^Review the current (?:git |working-tree )/i.test(trimmed)
+ trimmed.startsWith('Scan the selected files in the current git changes') ||
+ trimmed.startsWith(
+ 'Perform a thorough code review of every selected file in the current git changes',
+ )
) {
return trimmed;
}
- return `${REVIEW_HOST_PREFIX}\n\n${trimmed}`;
+ return `${prefix}\n\n${trimmed}`;
}
const EMBEDDING_SOURCES = [
@@ -461,6 +474,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
private view?: vscode.WebviewView;
private runCancel?: vscode.CancellationTokenSource;
+ private liveApprovalMode?: string;
private pendingResume?: {
resolve: (value: MitiiResumeInput | 'stop') => void;
};
@@ -1481,6 +1495,17 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
resolve('stop');
}
+ private waitForSuspensionResume(suspension: SuspensionPayload): Promise {
+ this.lastSuspensionRunId = suspension.runId;
+ this.pendingSuspension = suspension;
+ this.post({ type: 'run.suspended', suspension });
+ return new Promise((resolve) => {
+ this.pendingResume = { resolve };
+ // Apply access changes made while a model/tool call was still in flight.
+ this.autoApprovePendingToolApprovalIfAllowed(this.liveApprovalMode);
+ });
+ }
+
private autoApprovePendingToolApprovalIfAllowed(
approvalMode: string | undefined,
): void {
@@ -1562,10 +1587,17 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
// workspace files and interleave in the same session log).
this.runCancel?.cancel();
this.runCancel?.dispose();
+ this.liveApprovalMode = undefined;
this.runCancel = new this.vs.CancellationTokenSource();
+ const reviewKind =
+ message.mode === 'review'
+ ? message.reviewKind === 'code'
+ ? 'code'
+ : 'changes'
+ : undefined;
let llmPrompt =
message.mode === 'review'
- ? buildReviewLlmPrompt(prompt)
+ ? buildReviewLlmPrompt(prompt, reviewKind ?? 'changes')
: prompt;
if (message.mode === 'review') {
const root = this.effectiveRoot();
@@ -1692,6 +1724,10 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
approvalMode: message.approvalMode,
pinnedPaths: message.pinnedPaths,
requiredSkillIds: message.requiredSkillIds,
+ excludedSkillIds:
+ message.mode === 'review' && reviewKind !== 'code'
+ ? ['code-review-and-quality']
+ : undefined,
requiredMcpServerIds: message.requiredMcpServerIds,
workspaceId: this.getWorkspaceId(),
workspaceState: this.host.workspaceState,
@@ -1833,12 +1869,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
this.post({ type: 'setPlan', plan: planView });
}
}
- this.lastSuspensionRunId = suspension.runId;
- this.pendingSuspension = suspension;
- this.post({ type: 'run.suspended', suspension });
- return new Promise((resolve) => {
- this.pendingResume = { resolve };
- });
+ return this.waitForSuspensionResume(suspension);
},
},
});
@@ -2190,6 +2221,8 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
...this.lastIndex,
message,
readiness: 'indexing',
+ discoveredFileCount: undefined,
+ progressStage: undefined,
};
this.post({ type: 'index.status', index: this.lastIndex });
}
@@ -2209,6 +2242,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
secretKey:
(await this.secrets.get('mitii.provider.apiKey')) ?? undefined,
});
+ this.discoveredModels = [];
const result = await testProviderConnection({
type: message.provider.type,
baseUrl: message.provider.baseUrl,
@@ -2421,6 +2455,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
? error.message
: 'Settings could not be saved.',
});
+ return;
}
// Echo full state after the quick ack so Save never waits on bootstrap.
await this.sendBootstrap();
@@ -2458,8 +2493,21 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
const target = this.configurationTarget();
const update = async (key: string, value: unknown): Promise => {
if (configValuesEqual(cfg.get(key), value)) return;
- await cfg.update(key, value, target);
+ try {
+ await cfg.update(key, value, target);
+ } catch (error) {
+ if (error instanceof Error && /not a registered configuration/i.test(error.message)) {
+ throw new Error(`${error.message} Reload the VS Code window to load the extension's updated settings schema, then save again.`);
+ }
+ throw error;
+ }
};
+ const approvalMode = message.approvalMode ?? message.ui?.approvalMode;
+ if (approvalMode !== undefined) {
+ await update('safety.approvalMode', approvalMode);
+ if (this.runCancel) this.liveApprovalMode = approvalMode;
+ this.autoApprovePendingToolApprovalIfAllowed(approvalMode);
+ }
if (message.provider) {
await this.writeProviderSettings(message.provider);
}
@@ -2473,6 +2521,12 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
if (message.ui.showReasoning !== undefined) {
await update('ui.showReasoning', message.ui.showReasoning);
}
+ if (message.ui.features?.codeReviewButton !== undefined) {
+ await update(
+ 'ui.features.codeReviewButton',
+ message.ui.features.codeReviewButton === true,
+ );
+ }
if (message.ui.developerEnabled !== undefined) {
await update('developer.enabled', message.ui.developerEnabled);
}
@@ -2601,11 +2655,6 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
}
}
}
- const approvalMode = message.approvalMode ?? message.ui?.approvalMode;
- if (approvalMode !== undefined) {
- await update('safety.approvalMode', approvalMode);
- this.autoApprovePendingToolApprovalIfAllowed(approvalMode);
- }
if (message.workspaceRootOverride !== undefined) {
await update('workspace.rootPathOverride', message.workspaceRootOverride);
}
@@ -2709,6 +2758,10 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
Extract['provider']
>,
): Promise {
+ const type = provider.type ?? this.vs.workspace.getConfiguration('mitii').get('provider.type');
+ if (type !== 'echo' && provider.model !== undefined && !provider.model.trim()) {
+ throw new Error('Choose a model before saving provider settings. Test connection to discover available models.');
+ }
if (provider.type !== undefined) {
await this.writeConfigValue('provider.type', provider.type);
}
@@ -3157,6 +3210,10 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
developerEnabled: cfg.get('developer.enabled') ?? false,
debugLogging: cfg.get('debug') ?? false,
modelIoLogging: readModelIoLoggingEnabled(cfg),
+ features: {
+ codeReviewButton:
+ cfg.get('ui.features.codeReviewButton') === true,
+ },
tokenBudget: readTokenBudgetSettings(
cfg,
resolveContextWindow(this.vs),
@@ -3423,7 +3480,17 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider {
force: options.force === true,
...(options.filePaths?.length ? { filePaths: options.filePaths } : {}),
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
- ...(options.onProgress ? { onProgress: options.onProgress } : {}),
+ onProgress: (progress) => {
+ options.onProgress?.(progress);
+ this.lastIndex = {
+ ...this.lastIndex,
+ progressStage: progress.stage,
+ discoveredFileCount: progress.fileCount ?? this.lastIndex.discoveredFileCount,
+ readiness: 'indexing',
+ message: progress.message,
+ };
+ this.post({ type: 'index.status', index: this.lastIndex });
+ },
semanticIndex: await resolveVsCodeSemanticIndexSettings(
this.vs,
this.secrets,
diff --git a/apps/vscode/tests/SETTINGS_COVERAGE.md b/apps/vscode/tests/SETTINGS_COVERAGE.md
new file mode 100644
index 00000000..759f5b2f
--- /dev/null
+++ b/apps/vscode/tests/SETTINGS_COVERAGE.md
@@ -0,0 +1,29 @@
+# Settings regression coverage
+
+Run `pnpm test` for the full suite, including the rendered webview tests. Run
+`pnpm test:webview` for the settings UI suite alone.
+
+| Suite | What it verifies |
+| --- | --- |
+| `settingsRoundTrip.test.ts` | Every catalog setting has a VS Code manifest registration (MCP uses the registered parent object). Every enum value, both boolean values, and representative text/numeric edits pass through the actual sidebar settings writer. Configuration and supported host snapshots reflect saved values. Hidden legacy/advanced settings are included here. |
+| `sidebarSettingsPersistence.test.ts` | Workspace write target, value normalization/clamping, save/bootstrap ordering, and preservation of provider drafts during connection testing. The configuration mock rejects unregistered keys. |
+| `webview-ui/tests/settings.test.tsx` | Mounts the real React App, changes rendered inputs, clicks Save, invokes the actual host writer against an in-memory VS Code configuration adapter, and reads host snapshots back. Scalar inputs are also remounted to verify persistence. Covers provider presets, custom/discovered models, missing-model validation, autocomplete, workspace inputs, all embedding sources, per-mode approvals/models, context switches, run budgets, logging, local loop controls, and both slider/number forms of Simple budgets. |
+| `settingsFields.test.ts` | Parsing, clamping, defaults, token-limit calculations, profile reconciliation, mode mappings, and navigation behavior. The former tests that only wrote into a test-local object were replaced by the host/UI suites above. |
+| `liveSandboxProcess.test.ts` | Subsequent commands observe access changes on the same process port; explicit sandbox network overrides remain authoritative. |
+| `packages/host/src/config/createHostLlmPorts.spec.ts` | Model discovery works before model selection for OpenAI-compatible, Anthropic, and Gemini providers; unsuccessful discovery is not reported as connected. Requests are mocked. |
+
+Access regressions include upgrading a pending approval, upgrading while a model
+call is in flight, switching back before the next approval, and preventing a stale
+global Full access value from bypassing a new run's mode default.
+
+Index regressions distinguish the last published file count from the current
+scan's discovered count, show the stage and truncation state, and preserve unsaved
+embedding/file-limit changes when progress messages arrive. Discovered files are
+not presented as an exact processed-files progress counter.
+
+These tests use jsdom and a VS Code configuration adapter, not a running VS Code
+extension host. Profile and MCP disk storage are mocked in the webview suite; API
+key prompts, real provider credentials/networking, native embedding availability,
+and installed-extension activation require integration/manual checks. Reload VS
+Code after updating the extension manifest so new settings registrations take
+effect. The old installed 2.7.x manifest lacks per-mode approval registrations.
diff --git a/apps/vscode/tests/helpers/settingsHarness.ts b/apps/vscode/tests/helpers/settingsHarness.ts
new file mode 100644
index 00000000..46bec62f
--- /dev/null
+++ b/apps/vscode/tests/helpers/settingsHarness.ts
@@ -0,0 +1,66 @@
+import { vi } from 'vitest';
+import manifest from '../../package.json';
+import { MitiiSidebarProvider } from '../../src/sidebar';
+const registered: Record = manifest.contributes.configuration.properties;
+
+export function createProviderHarness() {
+ const store = new Map([
+ ['provider.type', 'echo'],
+ ['provider.preset', 'echo'],
+ ['provider.baseUrl', ''],
+ ['provider.model', 'echo'],
+ ]);
+ const updates: Array<{ key: string; value: unknown; target: unknown }> = [];
+ const cfg = {
+ inspect: (key: string) => ({ key, workspaceValue: store.get(key) }),
+ get: (key: string, fallback?: unknown) =>
+ store.has(key) ? store.get(key) : fallback,
+ update: vi.fn(async (key: string, value: unknown, target: unknown) => {
+ if (!registered[`mitii.${key}`]) throw new Error(`${key} is not a registered configuration`);
+ updates.push({ key, value, target });
+ if (value === undefined) {
+ store.delete(key);
+ } else {
+ store.set(key, value);
+ }
+ }),
+ };
+ const vs = {
+ ConfigurationTarget: { Global: 'global', Workspace: 'workspace' },
+ ExtensionMode: { Development: 1 },
+ Uri: { file: (path: string) => ({ fsPath: path, scheme: 'file' }) },
+ workspace: {
+ workspaceFolders: [{ uri: { fsPath: '/tmp/workspace' } }],
+ getConfiguration: () => cfg,
+ },
+ window: {
+ showInformationMessage: vi.fn(),
+ },
+ };
+ const provider = new MitiiSidebarProvider(
+ vs as never,
+ { fsPath: '/tmp/ext', scheme: 'file' } as never,
+ async () => ({}) as never,
+ () => undefined,
+ () => 'workspace',
+ { appendLine: vi.fn(), show: vi.fn() } as never,
+ { get: vi.fn(async () => undefined) } as never,
+ vi.fn(),
+ async () => ({ fileCount: 0, truncated: false }),
+ {
+ extensionMode: 1 as never,
+ workspaceState: { get: vi.fn(), update: vi.fn() } as never,
+ inlineDiff: { setPending: vi.fn() } as never,
+ reviewFindings: {} as never,
+ onInlineDiffPending: vi.fn(),
+ },
+ );
+ (provider as unknown as { sendBootstrap: () => Promise }).sendBootstrap =
+ async () => undefined;
+ (
+ provider as unknown as {
+ refreshDiscoveredModels: () => Promise;
+ }
+ ).refreshDiscoveredModels = async () => undefined;
+ return { provider, updates, store, cfg, target: vs.ConfigurationTarget.Workspace };
+}
diff --git a/apps/vscode/tests/liveSandboxProcess.test.ts b/apps/vscode/tests/liveSandboxProcess.test.ts
new file mode 100644
index 00000000..4ed45f68
--- /dev/null
+++ b/apps/vscode/tests/liveSandboxProcess.test.ts
@@ -0,0 +1,96 @@
+import { describe, expect, it, vi } from 'vitest';
+import { createLiveSandboxedProcessPort } from '../src/liveSandboxProcess';
+import type { SandboxBackend } from '@mitii/host';
+
+function harness() {
+ const settings = new Map([['safety.approvalMode', 'guided']]);
+ const vs = {
+ workspace: {
+ getConfiguration: () => ({
+ get: (key: string) => settings.get(key),
+ inspect: (key: string) =>
+ settings.has(key) ? { workspaceValue: settings.get(key) } : {},
+ }),
+ },
+ };
+ const wrap = vi.fn((request) => request);
+ const backend: SandboxBackend = { id: 'seatbelt', available: true, wrap };
+ const inner = {
+ execFile: vi.fn(async () => ({
+ exitCode: 0,
+ stdout: '',
+ stderr: '',
+ timedOut: false,
+ cancelled: false,
+ truncated: false,
+ })),
+ };
+ const port = createLiveSandboxedProcessPort(
+ vs as never,
+ inner,
+ '/workspace',
+ backend,
+ );
+ return { settings, wrap, port };
+}
+
+const request = {
+ argv: ['git', 'status'],
+ cwd: '/workspace',
+ timeoutMs: 1000,
+ maxOutputBytes: 1024,
+};
+
+describe('live command access settings', () => {
+ it('applies full access and later restricted access on the same process port', async () => {
+ const { settings, wrap, port } = harness();
+ for (const [mode, network] of [
+ ['guided', 'deny'],
+ ['pilot', 'allow'],
+ ['safe', 'deny'],
+ ] as const) {
+ settings.set('safety.approvalMode', mode);
+ await port.execFile(request);
+ expect(wrap).toHaveBeenLastCalledWith(
+ request,
+ expect.objectContaining({ enabled: true, network }),
+ );
+ }
+ });
+
+ it('preserves an explicit network override when access changes', async () => {
+ const { settings, wrap, port } = harness();
+ settings.set('safety.sandbox.network', 'deny');
+ settings.set('safety.approvalMode', 'pilot');
+ await port.execFile(request);
+ expect(wrap).toHaveBeenLastCalledWith(
+ request,
+ expect.objectContaining({ network: 'deny' }),
+ );
+ });
+
+ it('reuses the sandboxed wrapper when policy is unchanged', async () => {
+ const { wrap, port } = harness();
+ await port.execFile(request);
+ await port.execFile(request);
+ expect(wrap).toHaveBeenCalledTimes(2);
+ expect(wrap.mock.calls[0]?.[1]).toBe(wrap.mock.calls[1]?.[1]);
+ });
+
+ it('ignores a live sandbox disable after sandbox has already been enabled', async () => {
+ const { settings, wrap, port } = harness();
+ settings.set('safety.approvalMode', 'guided');
+ await port.execFile(request);
+ expect(wrap).toHaveBeenLastCalledWith(
+ request,
+ expect.objectContaining({ enabled: true }),
+ );
+
+ settings.set('safety.sandbox.enabled', false);
+ await port.execFile(request);
+ expect(wrap).toHaveBeenLastCalledWith(
+ request,
+ expect.objectContaining({ enabled: true }),
+ );
+ });
+});
diff --git a/apps/vscode/tests/settingsFields.test.ts b/apps/vscode/tests/settingsFields.test.ts
index 2e977e58..885e47fc 100644
--- a/apps/vscode/tests/settingsFields.test.ts
+++ b/apps/vscode/tests/settingsFields.test.ts
@@ -69,6 +69,7 @@ const BASE_UI: UiSettingsSnapshot = {
developerEnabled: false,
debugLogging: false,
modelIoLogging: false,
+ features: { codeReviewButton: false },
tokenBudget: {
enabled: false,
policy: {},
@@ -156,46 +157,6 @@ const BASE_UI: UiSettingsSnapshot = {
},
};
-function emptyStore(): Record {
- return {};
-}
-
-function writeField(
- store: Record,
- setting: string,
- value: unknown,
-): void {
- store[setting] = value;
-}
-
-function readField(store: Record, setting: string): unknown {
- return store[setting];
-}
-
-function editSaveReflect(fieldId: string, edited: unknown): unknown {
- const field = SETTINGS_FIELDS.find((entry) => entry.id === fieldId);
- if (!field) throw new Error(`Unknown field ${fieldId}`);
- const store = emptyStore();
- let persisted: unknown = edited;
- if (field.kind === 'int') {
- persisted = normalizeTokenLimit(edited);
- if (field.min !== undefined && field.min > 0) {
- persisted = Math.max(field.min, Number(persisted) || field.min);
- }
- } else if (field.kind === 'number') {
- const parsed = Number(edited);
- persisted = Number.isFinite(parsed) ? parsed : field.sample;
- if (field.min !== undefined) {
- persisted = Math.max(field.min, Number(persisted));
- }
- if (field.max !== undefined) {
- persisted = Math.min(field.max, Number(persisted));
- }
- }
- writeField(store, field.setting, persisted);
- return readField(store, field.setting);
-}
-
describe('settings field catalog', () => {
it('covers every settings page', () => {
const pages = new Set(SETTINGS_FIELDS.map((field) => field.page));
@@ -203,6 +164,7 @@ describe('settings field catalog', () => {
'autocomplete',
'context',
'developer',
+ 'features',
'mcp',
'modes',
'provider',
@@ -221,19 +183,6 @@ describe('settings field catalog', () => {
expect(budgetIds).toEqual(visible);
});
- it.each(SETTINGS_FIELDS.map((field) => [field.id, field] as const))(
- 'edits, saves, and reflects %s',
- (_id, field) => {
- const reflected = editSaveReflect(field.id, field.sample);
- if (field.kind === 'int') {
- const expected = Math.max(field.min ?? 0, Math.floor(Number(field.sample)));
- expect(reflected).toBe(expected);
- } else {
- expect(reflected).toEqual(field.sample);
- }
- expect(field.reflect).toBe('raw');
- },
- );
});
describe('context window edit / save / reflect', () => {
@@ -532,6 +481,14 @@ describe('modes fields', () => {
expect(reflected.reasoningPreviewMaxChars).toBe(4000);
});
+ it('saves the Code Review feature toggle', () => {
+ const next = applyUiPatch(BASE_UI, {
+ features: { codeReviewButton: true },
+ });
+ expect(next.features.codeReviewButton).toBe(true);
+ expect(reflectUiAfterSave(next).features.codeReviewButton).toBe(true);
+ });
+
it('saves run budget caps and reflects them', () => {
const next = applyUiPatch(BASE_UI, {
runBudget: {
@@ -639,18 +596,6 @@ describe('developer fields', () => {
});
});
-describe('workspace override', () => {
- it('saves a trimmed override and can clear it', () => {
- const store = emptyStore();
- writeField(store, 'workspace.rootPathOverride', '/tmp/mitii-workspace');
- expect(readField(store, 'workspace.rootPathOverride')).toBe(
- '/tmp/mitii-workspace',
- );
- writeField(store, 'workspace.rootPathOverride', null);
- expect(readField(store, 'workspace.rootPathOverride')).toBeNull();
- });
-});
-
describe('compact settings nav tooltips', () => {
it('collapses the left bar at the compact breakpoint', () => {
expect(isSettingsNavCompact(300)).toBe(true);
@@ -678,6 +623,7 @@ describe('compact settings nav tooltips', () => {
'workspace',
'modes',
'context',
+ 'features',
'integrations',
'debug',
]);
diff --git a/apps/vscode/tests/settingsRoundTrip.test.ts b/apps/vscode/tests/settingsRoundTrip.test.ts
new file mode 100644
index 00000000..ad242213
--- /dev/null
+++ b/apps/vscode/tests/settingsRoundTrip.test.ts
@@ -0,0 +1,105 @@
+import { readFileSync } from 'node:fs';
+import { describe, expect, it, vi } from 'vitest';
+import { SETTINGS_FIELDS } from '../src/settingsFields';
+import { createProviderHarness } from './helpers/settingsHarness';
+
+const properties = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).contributes.configuration.properties;
+function nested(path: string[], value: unknown): any {
+ return path.reduceRight((child, key) => ({ [key]: child }), value);
+}
+function patchFor(id: string, value: unknown): any {
+ if (id.startsWith('provider.') || id.startsWith('autocomplete.')) return nested(id.split('.'), value);
+ if (id === 'workspace.rootPathOverride') return { workspaceRootOverride: value };
+ if (id === 'workspace.maximumIndexFiles') return { workspaceMaximumIndexFiles: value };
+ if (id === 'semanticIndex.source') return { semanticIndex: { source: value } };
+ if (id.startsWith('ui.')) return nested(id.split('.'), value);
+ if (id.startsWith('runBudget.')) return { ui: nested(id.split('.'), value) };
+ for (const group of ['tokenBudget', 'loopPolicy']) {
+ if (id.startsWith(`${group}.`)) {
+ const key = id.slice(group.length + 1);
+ return { ui: { [group]: key === 'enabled' ? { enabled: value } : { enabled: true, [group === 'tokenBudget' ? 'policy' : 'thresholds']: { [key]: value } } } };
+ }
+ }
+ const names: Record = { 'developer.enabled': 'developerEnabled', 'developer.intensityOverrides': 'intensityOverrides', 'developer.modelIo': 'modelIoLogging', debug: 'debugLogging' };
+ if (names[id]) return { ui: { [names[id]]: value } };
+ throw new Error(`No real save mapping for ${id}`);
+}
+const editable = SETTINGS_FIELDS.filter(f => !['semanticIndex.backend', 'semanticIndex.enabled', 'mcp.enabled'].includes(f.id));
+const cases = editable.flatMap(field => {
+ const schema = properties[`mitii.${field.setting}`];
+ const values = field.kind === 'boolean' ? [false, true] : schema?.enum ?? [field.sample];
+ return values.map((value: unknown) => [field.id, value] as const);
+});
+describe('registered settings through the actual sidebar writer', () => {
+ it.each(SETTINGS_FIELDS.map(f => [f.id, f.setting]))('%s has a VS Code registration', (_id, key) => {
+ expect(properties[`mitii.${key === 'mcp.enabled' ? 'mcp' : key}`]).toBeDefined();
+ });
+ it.each(cases)('saves %s = %s and reads it back from configuration', async (id, value) => {
+ const { provider, store, cfg } = createProviderHarness();
+ store.set('developer.intensityOverrides', true);
+ const host = provider as any;
+ await host.applySettingsSet({ type: 'settings.set', ...patchFor(id, value) });
+ expect(cfg.get(id), id).toEqual(value);
+ const ui = host.readUi();
+ if (id.startsWith('ui.')) {
+ expect(id.slice(3).split('.').reduce((v: any, k) => v[k], ui)).toEqual(value);
+ } else if (id.startsWith('autocomplete.')) {
+ expect(host.readAutocomplete()[id.slice(13)]).toEqual(value);
+ } else if (id.startsWith('provider.')) {
+ expect((await host.readProvider())[id.slice(9)]).toEqual(value);
+ } else if (id === 'semanticIndex.source') {
+ expect((await host.withEmbedding({ fileCount: 0, truncated: false })).embeddingSource).toEqual(value);
+ }
+ });
+
+ it('applies full access to a pending approval even if stale mode registration fails', async () => {
+ const { provider, cfg, store } = createProviderHarness();
+ const host = provider as any;
+ const resume = vi.fn();
+ host.pendingResume = { resolve: resume };
+ host.pendingSuspension = { kind: 'approval_required', runId: 'run-1', approval: { approvalId: 'approval-1' } };
+ const update = cfg.update;
+ cfg.update = vi.fn(async (key, value, target) => {
+ if (key.startsWith('ui.modeDefaults')) throw new Error(`${key} is not a registered configuration`);
+ return update(key, value, target);
+ });
+ await expect(host.applySettingsSet({ type: 'settings.set', approvalMode: 'pilot', ui: { modeDefaults: { agent: { approvalMode: 'pilot' } } } })).rejects.toThrow(/Reload the VS Code window/);
+ expect(store.get('safety.approvalMode')).toBe('pilot');
+ expect(resume).toHaveBeenCalledWith(expect.objectContaining({ approvalMode: 'never', approval: { approvalId: 'approval-1', decision: 'approved' } }));
+ });
+});
+
+
+describe('access changes during an active run', () => {
+ const suspension = { kind: 'approval_required', runId: 'run-2', approval: { approvalId: 'approval-2' } };
+ it('applies full access selected while a model call was in flight to the next approval', async () => {
+ const { provider } = createProviderHarness();
+ const host = provider as any;
+ host.runCancel = {};
+ await host.applySettingsSet({ type: 'settings.set', approvalMode: 'pilot' });
+ await expect(host.waitForSuspensionResume(suspension)).resolves.toMatchObject({ approvalMode: 'never', approval: { decision: 'approved' } });
+ });
+ it('does not use stale global Full access to bypass a new run mode default', async () => {
+ const { provider, store } = createProviderHarness();
+ const host = provider as any;
+ store.set('safety.approvalMode', 'pilot');
+ const result = host.waitForSuspensionResume(suspension);
+ const resolved = vi.fn();
+ result.then(resolved);
+ await Promise.resolve();
+ expect(resolved).not.toHaveBeenCalled();
+ host.pendingResume.resolve('stop');
+ await result;
+ });
+ it('does not auto-approve after switching back from full access', async () => {
+ const { provider } = createProviderHarness();
+ const host = provider as any;
+ host.runCancel = {};
+ await host.applySettingsSet({ type: 'settings.set', approvalMode: 'pilot' });
+ await host.applySettingsSet({ type: 'settings.set', approvalMode: 'guided' });
+ const result = host.waitForSuspensionResume(suspension);
+ expect(host.pendingResume).toBeDefined();
+ host.pendingResume.resolve('stop');
+ await expect(result).resolves.toBe('stop');
+ });
+});
diff --git a/apps/vscode/tests/sidebarSettingsPersistence.test.ts b/apps/vscode/tests/sidebarSettingsPersistence.test.ts
index ae1011a9..6918598b 100644
--- a/apps/vscode/tests/sidebarSettingsPersistence.test.ts
+++ b/apps/vscode/tests/sidebarSettingsPersistence.test.ts
@@ -3,65 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
import { MitiiSidebarProvider } from '../src/sidebar';
import type { UiSettingsSnapshot } from '../src/protocol';
-function createProviderHarness() {
- const store = new Map([
- ['provider.type', 'echo'],
- ['provider.preset', 'echo'],
- ['provider.baseUrl', ''],
- ['provider.model', 'echo'],
- ]);
- const updates: Array<{ key: string; value: unknown; target: unknown }> = [];
- const cfg = {
- get: (key: string, fallback?: unknown) =>
- store.has(key) ? store.get(key) : fallback,
- update: vi.fn(async (key: string, value: unknown, target: unknown) => {
- updates.push({ key, value, target });
- if (value === undefined) {
- store.delete(key);
- } else {
- store.set(key, value);
- }
- }),
- };
- const vs = {
- ConfigurationTarget: { Global: 'global', Workspace: 'workspace' },
- ExtensionMode: { Development: 1 },
- Uri: { file: (path: string) => ({ fsPath: path, scheme: 'file' }) },
- workspace: {
- workspaceFolders: [{ uri: { fsPath: '/tmp/workspace' } }],
- getConfiguration: () => cfg,
- },
- window: {
- showInformationMessage: vi.fn(),
- },
- };
- const provider = new MitiiSidebarProvider(
- vs as never,
- { fsPath: '/tmp/ext', scheme: 'file' } as never,
- async () => ({}) as never,
- () => undefined,
- () => 'workspace',
- { appendLine: vi.fn(), show: vi.fn() } as never,
- { get: vi.fn(async () => undefined) } as never,
- vi.fn(),
- async () => ({ fileCount: 0, truncated: false }),
- {
- extensionMode: 1 as never,
- workspaceState: { get: vi.fn(), update: vi.fn() } as never,
- inlineDiff: {} as never,
- reviewFindings: {} as never,
- onInlineDiffPending: vi.fn(),
- },
- );
- (provider as unknown as { sendBootstrap: () => Promise }).sendBootstrap =
- async () => undefined;
- (
- provider as unknown as {
- refreshDiscoveredModels: () => Promise;
- }
- ).refreshDiscoveredModels = async () => undefined;
- return { provider, updates, target: vs.ConfigurationTarget.Workspace };
-}
+import { createProviderHarness } from './helpers/settingsHarness';
const UI_PATCH: Partial = {
showReasoning: false,
@@ -69,6 +11,7 @@ const UI_PATCH: Partial = {
intensityOverrides: true,
debugLogging: true,
modelIoLogging: true,
+ features: { codeReviewButton: true },
reasoningPreviewMaxChars: 4000,
depth: 'deep',
effort: 'high',
@@ -181,6 +124,7 @@ describe('MitiiSidebarProvider settings persistence', () => {
'autocomplete.suffixChars',
'autocomplete.temperature',
'ui.showReasoning',
+ 'ui.features.codeReviewButton',
'developer.enabled',
'developer.intensityOverrides',
'debug',
diff --git a/apps/vscode/webview-ui/package.json b/apps/vscode/webview-ui/package.json
index 81b6e62e..a6c6fa63 100644
--- a/apps/vscode/webview-ui/package.json
+++ b/apps/vscode/webview-ui/package.json
@@ -6,7 +6,8 @@
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
- "typecheck": "tsc -p tsconfig.json --noEmit"
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "vitest run --config vitest.config.ts"
},
"dependencies": {
"react": "^18.3.1",
@@ -15,9 +16,12 @@
"remark-gfm": "^4.0.1"
},
"devDependencies": {
+ "@testing-library/react": "^16.3.3",
+ "@testing-library/user-event": "^14.6.7",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^26.1.0",
"typescript": "^5.5.2",
"vite": "^5.4.11"
}
diff --git a/apps/vscode/webview-ui/src/App.tsx b/apps/vscode/webview-ui/src/App.tsx
index 0d31dcdd..8bb4f653 100644
--- a/apps/vscode/webview-ui/src/App.tsx
+++ b/apps/vscode/webview-ui/src/App.tsx
@@ -129,8 +129,12 @@ const EMPTY_TOKEN_USAGE: TokenUsageSnapshot = {
};
const REVIEW_SKILL_ID = 'code-review-and-quality';
+/** Quick diff scan — structured findings only, no quality skill. */
const DEFAULT_REVIEW_PROMPT =
- 'Review the current working-tree changes. Prefer high-signal bugs and security issues.';
+ 'Scan the working-tree diff for material bugs, regressions, and security issues only.';
+/** Thorough code review — attaches code-review-and-quality. */
+const DEFAULT_CODE_REVIEW_PROMPT =
+ 'Perform a thorough code review of the current working-tree changes across correctness, readability, architecture, tests, and risk.';
function mergeReviewSkillIds(ids: string[]): string[] {
const without = ids.filter((id) => id !== REVIEW_SKILL_ID);
@@ -245,6 +249,7 @@ const DEFAULT_UI: UiSettingsSnapshot = {
developerEnabled: false,
debugLogging: false,
modelIoLogging: false,
+ features: { codeReviewButton: false },
tokenBudget: DEFAULT_TOKEN_BUDGET,
loopPolicy: DEFAULT_LOOP_POLICY,
policyLab: DEFAULT_POLICY_LAB,
@@ -299,6 +304,10 @@ function hydrateUiSnapshot(
return {
...DEFAULT_UI,
...(raw ?? {}),
+ features: {
+ ...DEFAULT_UI.features,
+ ...(raw?.features ?? {}),
+ },
modeDefaults: {
...DEFAULT_UI.modeDefaults,
...(raw?.modeDefaults ?? {}),
@@ -444,11 +453,15 @@ function mergeUiPatch(
tokenBudget: _tb,
loopPolicy: _lp,
policyLab: _pl,
+ features: _features,
...scalarPatch
} = patch;
return {
...base,
...scalarPatch,
+ features: patch.features
+ ? { ...base.features, ...patch.features }
+ : base.features,
contextToggles: patch.contextToggles
? { ...base.contextToggles, ...patch.contextToggles }
: base.contextToggles,
@@ -907,6 +920,8 @@ export function App() {
const [connectionMessage, setConnectionMessage] = useState(
null,
);
+ const maximumIndexFilesDraftRef = useRef(undefined);
+ const embeddingSourceDraftRef = useRef(undefined);
const [customModel, setCustomModel] = useState(false);
const [index, setIndex] = useState({
fileCount: 0,
@@ -945,9 +960,9 @@ export function App() {
>([]);
const [reviewBarExpandToken, setReviewBarExpandToken] = useState(0);
const pendingAutoReviewRef = useRef<{ prompt?: string } | null>(null);
- const runReviewRef = useRef<((promptOverride?: string) => void) | null>(
- null,
- );
+ const runReviewRef = useRef<
+ ((promptOverride?: string, options?: { codeReview?: boolean }) => void) | null
+ >(null);
const [skillItems, setSkillItems] = useState([]);
const [automationSpecs, setAutomationSpecs] = useState(
[],
@@ -1071,6 +1086,23 @@ export function App() {
[],
);
+ const applyIndexStatus = useCallback((incoming: IndexStatusSnapshot) => {
+ const next = {
+ ...incoming,
+ ...(maximumIndexFilesDraftRef.current !== undefined
+ ? { maximumIndexFiles: maximumIndexFilesDraftRef.current }
+ : {}),
+ ...(embeddingSourceDraftRef.current
+ ? {
+ embeddingSource: embeddingSourceDraftRef.current,
+ embeddingEnabled: embeddingSourceDraftRef.current !== 'disabled',
+ }
+ : {}),
+ };
+ indexRef.current = next;
+ setIndex(next);
+ }, []);
+
const applyBootstrap = useCallback((msg: HostToWebviewMessage) => {
if (msg.type === 'bootstrap' || msg.type === 'settings') {
setWorkspace(msg.workspace);
@@ -1176,7 +1208,7 @@ export function App() {
settingsSavingRef.current = false;
setSettingsSaving(false);
if (msg.type === 'bootstrap') {
- setIndex(msg.index);
+ applyIndexStatus(msg.index);
setOnboardingRequired(msg.onboardingRequired);
setHistory(msg.history);
setActiveThreadId(msg.activeThreadId);
@@ -1250,7 +1282,7 @@ export function App() {
applyBootstrap(msg);
break;
case 'index.status':
- setIndex(msg.index);
+ applyIndexStatus(msg.index);
break;
case 'run.started': {
setRunning(true);
@@ -1621,6 +1653,10 @@ export function App() {
}));
break;
case 'settings.saved':
+ if (settingsSavingRef.current && msg.ok) {
+ embeddingSourceDraftRef.current = undefined;
+ maximumIndexFilesDraftRef.current = undefined;
+ }
settingsSavingRef.current = false;
setSettingsSaving(false);
if (!msg.ok) {
@@ -1639,7 +1675,7 @@ export function App() {
postToHost({ type: 'ready' });
}
return off;
- }, [applyBootstrap, applyTokenUsage, markSuspensionResumed]);
+ }, [applyBootstrap, applyIndexStatus, applyTokenUsage, markSuspensionResumed]);
useLayoutEffect(() => {
const turnCountChanged = turns.length !== lastTurnCountRef.current;
@@ -1693,9 +1729,13 @@ export function App() {
}, [prompt, running, mode, ui, approvalMode, pinned, pinnedSkillIds, pinnedMcpServerIds]);
const runReview = useCallback(
- (promptOverride?: string) => {
+ (promptOverride?: string, options?: { codeReview?: boolean }) => {
if (running) return;
- const text = (promptOverride ?? prompt).trim() || DEFAULT_REVIEW_PROMPT;
+ const codeReview = options?.codeReview === true;
+ const fallback = codeReview
+ ? DEFAULT_CODE_REVIEW_PROMPT
+ : DEFAULT_REVIEW_PROMPT;
+ const text = (promptOverride ?? prompt).trim() || fallback;
stickToBottomRef.current = true;
forceScrollToBottomRef.current = true;
const defaults = modeDefaultsFromUi(ui, 'ask');
@@ -1705,18 +1745,21 @@ export function App() {
depth: defaults.depth,
effort: ui.effort,
});
- // Keep Ask/Plan/Agent selection; host maps mode:'review' → engine ask.
+ // Review button sends mode:'review'; keep Ask/Plan/Agent selection.
if (mode === 'review') setMode('ask');
setReviewBarExpandToken((n) => n + 1);
postToHost({
type: 'ask',
prompt: text,
mode: 'review',
+ reviewKind: codeReview ? 'code' : 'changes',
depth: intensity.depth,
effort: intensity.effort,
approvalMode: defaults.approvalMode,
pinnedPaths: pinned.map((p) => p.path),
- requiredSkillIds: mergeReviewSkillIds(pinnedSkillIds),
+ requiredSkillIds: codeReview
+ ? mergeReviewSkillIds(pinnedSkillIds)
+ : pinnedSkillIds.filter((id) => id !== REVIEW_SKILL_ID),
requiredMcpServerIds: pinnedMcpServerIds,
});
setPrompt('');
@@ -2305,6 +2348,12 @@ export function App() {
const saveAllSettings = () => {
(document.activeElement as HTMLElement | null)?.blur?.();
const latestProvider = snapshotProvider();
+ if (latestProvider.type !== 'echo' && !latestProvider.model.trim()) {
+ setError('Choose a model before saving provider settings. Test connection to discover available models.');
+ setSettingsTab('model');
+ return;
+ }
+ setError(null);
const latestUi = clearStaleModeModelDefaultsAfterProviderModelChange({
ui: mergeUiPatch(
uiRef.current,
@@ -2331,6 +2380,9 @@ export function App() {
ui: latestUi,
workspaceRootOverride: overrideDraft.trim() || null,
workspaceMaximumIndexFiles: indexRef.current.maximumIndexFiles ?? 0,
+ ...(embeddingSourceDraftRef.current
+ ? { semanticIndex: { source: embeddingSourceDraftRef.current } }
+ : {}),
mcp,
approvalMode,
profile: {
@@ -2659,6 +2711,12 @@ export function App() {
postToHost({ type: 'openFile', path, line })
}
onRunReview={() => runReview()}
+ onRunCodeReview={
+ ui.features.codeReviewButton
+ ? () => runReview(undefined, { codeReview: true })
+ : undefined
+ }
+ showCodeReview={ui.features.codeReviewButton === true}
onUndoAll={() => {
const changes = [...turns]
.reverse()
@@ -3060,6 +3118,7 @@ export function App() {
0,
Math.min(240000, Math.floor(value)),
);
+ maximumIndexFilesDraftRef.current = maximumIndexFiles;
indexRef.current = {
...indexRef.current,
maximumIndexFiles,
@@ -3070,12 +3129,17 @@ export function App() {
}));
}}
onEmbeddingSourceChange={(source: SemanticIndexSource) => {
+ embeddingSourceDraftRef.current = source;
+ indexRef.current = {
+ ...indexRef.current,
+ embeddingSource: source,
+ embeddingEnabled: source !== 'disabled',
+ };
setIndex((current) => ({
...current,
embeddingSource: source,
embeddingEnabled: source !== 'disabled',
}));
- postToHost({ type: 'settings.set', semanticIndex: { source } });
}}
memories={memories}
onAddMemory={(text) => postToHost({ type: 'addMemory', text })}
diff --git a/apps/vscode/webview-ui/src/components/ComposerControls.tsx b/apps/vscode/webview-ui/src/components/ComposerControls.tsx
index ccc438d4..08b7fbf8 100644
--- a/apps/vscode/webview-ui/src/components/ComposerControls.tsx
+++ b/apps/vscode/webview-ui/src/components/ComposerControls.tsx
@@ -114,7 +114,7 @@ export const MODE_HINT: Record = {
ask: 'Explore and answer — read-only.',
plan: 'Analyze and propose a structured plan.',
agent: 'Implement changes with controlled execution.',
- review: 'Structured working-tree review (use the Review bar above the chat).',
+ review: 'Git working-tree review via the Review button — not a chat mode.',
};
interface ComposerControlsProps {
diff --git a/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx b/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
index 592f70c8..bfdac095 100644
--- a/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
+++ b/apps/vscode/webview-ui/src/components/IndexingStatusBar.tsx
@@ -103,9 +103,15 @@ function shortLabel(tone: IndexTone, index: IndexStatusSnapshot): string {
function detailTooltip(index: IndexStatusSnapshot): string {
const parts: string[] = [];
- if (index.fileCount > 0) {
+ if (index.fileCount >= 0) {
parts.push(`${index.fileCount.toLocaleString()} files indexed`);
}
+ if (index.discoveredFileCount !== undefined) {
+ parts.push(
+ `${index.discoveredFileCount.toLocaleString()} files discovered in current scan`,
+ );
+ }
+ if (index.progressStage) parts.push(`Stage: ${index.progressStage}`);
if (index.readiness) parts.push(`Readiness: ${index.readiness}`);
if (index.scanCompleteness) parts.push(`Scan: ${index.scanCompleteness}`);
if (index.indexMode) {
@@ -142,11 +148,11 @@ export function IndexingStatusBar({
return (
diff --git a/apps/vscode/webview-ui/src/components/MessageList.tsx b/apps/vscode/webview-ui/src/components/MessageList.tsx
index 8a31730b..d8a9b3b8 100644
--- a/apps/vscode/webview-ui/src/components/MessageList.tsx
+++ b/apps/vscode/webview-ui/src/components/MessageList.tsx
@@ -87,7 +87,7 @@ const MODE_LABELS: Record = {
ask: 'Ask mode',
plan: 'Plan mode',
agent: 'Agent mode',
- review: 'Review mode',
+ review: 'Working-tree review',
};
export function MessageList({
diff --git a/apps/vscode/webview-ui/src/components/NumberField.tsx b/apps/vscode/webview-ui/src/components/NumberField.tsx
index 9813eb84..df2a8853 100644
--- a/apps/vscode/webview-ui/src/components/NumberField.tsx
+++ b/apps/vscode/webview-ui/src/components/NumberField.tsx
@@ -31,6 +31,7 @@ export function NumberField({
const [draft, setDraft] = useState(String(safeValue));
const draftRef = useRef(draft);
const focusedRef = useRef(false);
+ const valueAtFocusRef = useRef(safeValue);
draftRef.current = draft;
useEffect(() => {
@@ -62,8 +63,14 @@ export function NumberField({
return;
}
setDraft(String(bounded));
- onDraftChange?.(bounded);
- if (bounded !== safeValue) onCommit(bounded);
+ // Compare against the value when focus began so live parent updates
+ // (e.g. auto-scaled max output mirroring the draft) do not swallow commits.
+ if (bounded !== valueAtFocusRef.current) {
+ onDraftChange?.(bounded);
+ onCommit(bounded);
+ return;
+ }
+ onDraftChange?.(undefined);
};
return (
@@ -82,6 +89,7 @@ export function NumberField({
value={draft}
onFocus={() => {
focusedRef.current = true;
+ valueAtFocusRef.current = safeValue;
}}
onChange={(e) => {
const nextDraft = e.target.value;
diff --git a/apps/vscode/webview-ui/src/components/ReviewPanel.tsx b/apps/vscode/webview-ui/src/components/ReviewPanel.tsx
index a63d7175..9ed8be34 100644
--- a/apps/vscode/webview-ui/src/components/ReviewPanel.tsx
+++ b/apps/vscode/webview-ui/src/components/ReviewPanel.tsx
@@ -14,7 +14,7 @@ interface ReviewPanelProps {
}
const DEFAULT_REVIEW_HINT =
- 'Review mode is read-only. Run a review below or type a focus (bugs, security, tests). Do not use Agent for review.';
+ 'Working-tree review is read-only. Use Run review on the Review bar (not a chat mode). Optional focus: bugs, security, tests.';
function reviewStatusLabel(status: string): string {
const normalized = status.trim() || '?';
diff --git a/apps/vscode/webview-ui/src/components/SettingsPanel.tsx b/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
index 6551134c..15f4197f 100644
--- a/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
+++ b/apps/vscode/webview-ui/src/components/SettingsPanel.tsx
@@ -47,6 +47,7 @@ import {
IconModel,
IconPlan,
IconPlug,
+ IconReview,
} from './Icons';
import { NumberField } from './NumberField';
import { TokenBudgetAllocation } from './TokenBudgetAllocation';
@@ -137,6 +138,7 @@ const NAV: {
{ id: 'workspace', label: 'Workspace', icon: },
{ id: 'modes', label: 'Modes', icon: },
{ id: 'context', label: 'Context', icon: },
+ { id: 'features', label: 'Features', icon: },
{ id: 'integrations', label: 'MCP', icon: },
{ id: 'debug', label: 'Developer', icon: },
];
@@ -162,6 +164,10 @@ const PAGE_COPY: Record = {
title: 'Context',
description: 'What Mitii attaches to each turn.',
},
+ features: {
+ title: 'Features',
+ description: 'Opt-in IDE surfaces. Off by default until you enable them.',
+ },
integrations: {
title: 'MCP',
description: 'Optional servers. Off by default.',
@@ -490,15 +496,16 @@ export function SettingsPanel(props: SettingsPanelProps) {
effective: provider.effectiveContextWindow,
fallback: ui.tokenBudget.preview.contextWindowTokens,
});
- const storedMaxOutput = normalizeMaximumOutputTokens(
- draftMaxOutput ?? provider.maximumOutputTokens,
+ const storedMaxOutput = normalizeMaximumOutputTokens(provider.maximumOutputTokens);
+ const previewMaxOutput = normalizeMaximumOutputTokens(
+ draftMaxOutput ?? storedMaxOutput,
);
- const autoMaxOutput = isAutoMaximumOutputTokens(storedMaxOutput);
+ const autoMaxOutput = isAutoMaximumOutputTokens(previewMaxOutput);
const livePreview = useMemo(() => {
try {
return deriveLiveTokenBudgetPreview({
contextWindowTokens: previewContextWindow,
- maximumOutputTokens: storedMaxOutput,
+ maximumOutputTokens: previewMaxOutput,
policy: ui.tokenBudget.enabled ? ui.tokenBudget.policy : undefined,
runBudget: ui.runBudget,
});
@@ -507,15 +514,17 @@ export function SettingsPanel(props: SettingsPanelProps) {
}
}, [
previewContextWindow,
- storedMaxOutput,
+ previewMaxOutput,
ui.runBudget,
ui.tokenBudget.enabled,
ui.tokenBudget.policy,
ui.tokenBudget.preview,
]);
+ // When max output is auto (0), show the window-derived reserve so it tracks
+ // context-window edits live. Typing a positive value pins an override.
const displayMaxOutput = autoMaxOutput
? livePreview.maximumOutputTokens
- : storedMaxOutput;
+ : previewMaxOutput;
return (
({ ...prev, model: next }));
}}
>
+
{options.map((id) => (
- {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.`}
+ When enabled, the review bar shows both{' '}
+ Review (quick diff findings) and{' '}
+ Code Review (thorough quality review with the
+ code-review skill). Off by default.
+
+
+
+ ) : null}
+
{activeTab === 'integrations' ? (
void;
onOpenFinding?: (path: string, line?: number) => void;
onRunReview: () => void;
+ /** Deeper code review — only when Settings → Features enables the button. */
+ onRunCodeReview?: () => void;
+ showCodeReview?: boolean;
onUndoAll?: () => void;
onKeepAll?: () => void;
/** Clear sticky review findings (Problems + comments + chips). */
@@ -78,6 +81,8 @@ export function WorkingTreeReviewBar({
onOpenDiff,
onOpenFinding,
onRunReview,
+ onRunCodeReview,
+ showCodeReview = false,
onUndoAll,
onKeepAll,
onDismissFindings,
@@ -229,11 +234,22 @@ export function WorkingTreeReviewBar({
type="button"
className="wt-review__cta"
disabled={running || fileCount === 0}
- title="Run a structured read-only review — findings open in the editor"
+ title="Quick diff scan for bugs/regressions — not a full code review"
onClick={onRunReview}
>
{running ? 'Reviewing…' : 'Review'}
+ {showCodeReview && onRunCodeReview ? (
+
+ ) : null}
- No findings yet. Click Review — issues appear on the code line
- in the editor (and in Problems).
+ No findings yet. Click Review
+ {showCodeReview ? ' or Code Review' : ''} — issues appear on the
+ code line in the editor (and in Problems).
- No working-tree diff. Edit files (or stage changes), click Refresh,
- then Run review.
+ No working-tree diff. Edit files (or stage changes), then click
+ Refresh to see changes.