diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe894a3..eb66b9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,16 @@ jobs: with: node-version: 20 cache: pnpm + - name: Install native build tools (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y build-essential python3 - run: pnpm install --frozen-lockfile + - run: pnpm run sync:versions -- --check - run: pnpm run release:check-assets - run: pnpm run typecheck + - run: pnpm run rebuild:node - run: pnpm test - run: pnpm run check:architecture - run: pnpm run build:cli + - run: pnpm run build:vscode - run: pnpm run benchmark:validate diff --git a/.github/workflows/native-binaries.yml b/.github/workflows/native-binaries.yml index ba56fda..389e783 100644 --- a/.github/workflows/native-binaries.yml +++ b/.github/workflows/native-binaries.yml @@ -24,9 +24,13 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 20 cache: pnpm + - name: Install native build tools (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y build-essential python3 - run: pnpm install --frozen-lockfile + - run: pnpm run sync:versions -- --check - run: pnpm run typecheck - run: pnpm run rebuild:native env: diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index ac09aba..a0baeb2 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,18 +1,45 @@ -# @mitii/v8, @mitii/sdk, and @mitii/cli ship with "private": true. -# Public npm publish is deferred until those flags are intentionally removed. -# Product releases today are VSIX artifacts via .github/workflows/release.yml. -name: npm publish (deferred) +# Publishes @mitii/v8 → @mitii/sdk → @mitii/host → @mitii/cli to npm. +# Invoked from the Release workflow on v* tags, or manually via workflow_dispatch. +# Requires repository secret: NPM_TOKEN +name: npm publish on: workflow_dispatch: + workflow_call: jobs: - deferred: + publish: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - - name: Packages are private + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + cache: pnpm + - name: Require NPM_TOKEN + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - echo "::notice::npm publish is deferred while packages remain private: true." - echo "Ship the VS Code extension via the Release workflow (VSIX)." - echo "To publish npm later: remove private flags, then restore a real publish job." - exit 1 + if [ -z "$NPM_TOKEN" ]; then + echo "::error::NPM_TOKEN secret is missing. Add an npm automation token in repo Settings → Secrets → Actions." + exit 1 + fi + - name: Install native build tools + run: sudo apt-get update && sudo apt-get install -y build-essential python3 + - run: pnpm install --frozen-lockfile + - run: pnpm run sync:versions -- --check + - run: pnpm run rebuild:node + - run: pnpm run build:v8 + - run: pnpm run build:sdk + - run: pnpm run build:host + - run: pnpm run build:cli + - run: pnpm run check:architecture + - name: Publish npm packages + run: node scripts/publish-npm.cjs + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 997f363..f9fe033 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,5 @@ +# Tag release: multi-platform VSIX + GitHub Release + Marketplace (+ Open VSX) + npm. +# Requires secrets: NPM_TOKEN, VSCE_PAT. Optional: OVSX_PAT. name: Release on: @@ -28,10 +30,15 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 20 cache: pnpm + - name: Install native build tools (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y build-essential python3 - run: pnpm install --frozen-lockfile + - run: pnpm run sync:versions -- --check - run: pnpm run typecheck + - run: pnpm run rebuild:node - run: pnpm test - run: pnpm run rebuild:native env: @@ -61,3 +68,41 @@ jobs: with: body_path: release-notes.md files: 'dist-vsix/*.vsix' + + publish-marketplace: + needs: package + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - name: Require VSCE_PAT + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + if [ -z "$VSCE_PAT" ]; then + echo "::error::VSCE_PAT secret is missing. Add an Azure DevOps PAT with Marketplace publish scope for publisher mitii." + exit 1 + fi + - run: pnpm install --frozen-lockfile + - uses: actions/download-artifact@v4 + with: + path: dist-vsix + merge-multiple: true + - name: Publish to VS Code Marketplace + run: node scripts/publish-vscode-vsix.cjs + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + - name: Publish to Open VSX + run: node scripts/publish-ovsx-vsix.cjs + env: + OVSX_PAT: ${{ secrets.OVSX_PAT }} + + npm: + uses: ./.github/workflows/npm-publish.yml + secrets: inherit diff --git a/README.md b/README.md index eadbf72..a01fe2f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ License: AGPL v3 VS Code 1.85+ Node 20+ - Version 2.8.0 + Version 2.8.5 Documentation

@@ -126,7 +126,7 @@ See [apps/cli/README.md](apps/cli/README.md) for `ask`, `session`, `index`, `sta ### SDK -`@mitii/sdk`, `@mitii/v8`, and `@mitii/cli` are **private workspace packages** today (not published to npm). Consume them from this monorepo, or wait until publish is intentionally enabled. +`@mitii/v8`, `@mitii/sdk`, `@mitii/host`, and `@mitii/cli` publish to npm on `v*` release tags (see [docs/RELEASE.md](docs/RELEASE.md)). For local development, consume them from this monorepo workspace. ```ts import { createMitiiClient, EchoLlmPort } from '@mitii/sdk'; diff --git a/apps/cli/README.md b/apps/cli/README.md index d6a30dc..4aab2f9 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -12,14 +12,14 @@ npx @mitii/cli --help Requires **Node.js 20+**. Native dependency: `better-sqlite3` (optional LanceDB for vectors). License: **AGPL-3.0-or-later**. -> Until published from this monorepo, build from source: +Published from this monorepo on `v*` release tags. For local development: ```bash pnpm --filter @mitii/cli build node apps/cli/bin/mitii.js --help ``` -Legacy npm `@mitii/cli@2.7.x` is a different binary stack — do not mix with this tree until publish replaces it. +Legacy npm `@mitii/cli@2.7.x` is a different binary stack — prefer versions published from this tree. ## Quick start diff --git a/apps/cli/package.json b/apps/cli/package.json index f9e4343..c16a17c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mitii/cli", - "version": "2.8.0", + "version": "2.8.5", "description": "Mitii headless CLI over @mitii/sdk.", "license": "AGPL-3.0-or-later", "publishConfig": { diff --git a/apps/cli/scripts/build-cli.cjs b/apps/cli/scripts/build-cli.cjs index 453ac41..24aa7f8 100644 --- a/apps/cli/scripts/build-cli.cjs +++ b/apps/cli/scripts/build-cli.cjs @@ -1,8 +1,8 @@ const { build } = require('esbuild'); const { createRequire } = require('node:module'); const { builtinModules } = require('node:module'); -const { mkdirSync } = require('node:fs'); -const { dirname, join } = require('node:path'); +const { cpSync, mkdirSync, rmSync } = require('node:fs'); +const { dirname, join, resolve } = require('node:path'); const root = join(__dirname, '..'); const outfile = join(root, 'dist/cli.js'); @@ -20,6 +20,14 @@ const externals = new Set([ mkdirSync(dirname(outfile), { recursive: true }); +function stageBundledSkills() { + const source = resolve(__dirname, '../../../packages/sdk/skills'); + const target = join(root, 'dist/skills'); + rmSync(target, { recursive: true, force: true }); + cpSync(source, target, { recursive: true }); + console.log(`staged ${target}`); +} + build({ absWorkingDir: root, entryPoints: [join(root, 'src/cli.ts')], @@ -53,6 +61,7 @@ build({ logLevel: 'info', }) .then(() => { + stageBundledSkills(); console.log(`built ${outfile}`); }) .catch((error) => { diff --git a/apps/vscode/package.json b/apps/vscode/package.json index b53d162..25fc64e 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.8.0", + "version": "2.8.5", "publisher": "mitii", "license": "AGPL-3.0-or-later", "icon": "media/mitii-short-logo.png", @@ -81,6 +81,7 @@ "files": [ "dist/extension.js", "dist/native/better_sqlite3.node", + "dist/skills/**/*", "dist/webview/**/*", "media/**/*", "README.md", @@ -257,9 +258,9 @@ }, "mitii.provider.contextWindow": { "type": "number", - "default": 32768, - "minimum": 1024, - "description": "Model context window in tokens. Used for prompt budgeting, max output reserve, and the token meter. Prefer the real window for your model (or leave unset to use the model preset)." + "default": 0, + "minimum": 0, + "description": "Model context window in tokens. Set 0 to use the model preset; use a positive value only when your runtime has a different limit." }, "mitii.provider.maximumOutputTokens": { "type": "number", diff --git a/apps/vscode/scripts/audit-package.cjs b/apps/vscode/scripts/audit-package.cjs index a0140a7..bc02f7c 100644 --- a/apps/vscode/scripts/audit-package.cjs +++ b/apps/vscode/scripts/audit-package.cjs @@ -43,6 +43,14 @@ assertFile(join(dist, 'webview', 'index.html'), 'webview index'); assertFile(join(dist, 'webview', 'main.js'), 'webview script'); assertFile(join(dist, 'webview', 'main.css'), 'webview style'); assertFile(join(dist, 'native', 'better_sqlite3.node'), 'SQLite native binding'); +assertFile( + join(dist, 'skills', 'planning-default', 'SKILL.md'), + 'bundled planning skill', +); +assertFile( + join(dist, 'skills', 'safety-always', 'SKILL.md'), + 'bundled safety skill', +); const bundlePath = join(dist, 'extension.js'); if (existsSync(bundlePath)) { diff --git a/apps/vscode/scripts/build-extension.cjs b/apps/vscode/scripts/build-extension.cjs index 587827e..58d7607 100644 --- a/apps/vscode/scripts/build-extension.cjs +++ b/apps/vscode/scripts/build-extension.cjs @@ -2,7 +2,13 @@ const { build } = require('esbuild'); const { spawnSync } = require('node:child_process'); const { createRequire } = require('node:module'); const { builtinModules } = require('node:module'); -const { existsSync, mkdirSync, rmSync, writeFileSync } = require('node:fs'); +const { + cpSync, + existsSync, + mkdirSync, + rmSync, + writeFileSync, +} = require('node:fs'); const { dirname, join, resolve } = require('node:path'); const { stageNativeSqliteBinding, @@ -13,7 +19,8 @@ const distDir = join(root, 'dist'); const outfile = join(root, 'dist/extension.js'); const webviewDir = join(root, 'webview-ui'); const requireFromApp = createRequire(join(root, 'package.json')); -const production = process.argv.includes('--production') || process.env.NODE_ENV === 'production'; +const production = + process.argv.includes('--production') || process.env.NODE_ENV === 'production'; const builtins = new Set([ ...builtinModules, ...builtinModules.map((name) => `node:${name}`), @@ -42,6 +49,14 @@ function buildWebview() { console.log(`built ${join(root, 'dist/webview')}`); } +function stageBundledSkills() { + const source = resolve(__dirname, '../../../packages/sdk/skills'); + const target = join(distDir, 'skills'); + rmSync(target, { recursive: true, force: true }); + cpSync(source, target, { recursive: true }); + console.log(`staged ${target}`); +} + rmSync(distDir, { recursive: true, force: true }); mkdirSync(dirname(outfile), { recursive: true }); @@ -88,6 +103,7 @@ build({ ); } stageNativeSqliteBinding(); + stageBundledSkills(); console.log(`built ${outfile}`); }) .catch((error) => { diff --git a/apps/vscode/src/conversationCarry.ts b/apps/vscode/src/conversationCarry.ts index 5b7a001..8d11a18 100644 --- a/apps/vscode/src/conversationCarry.ts +++ b/apps/vscode/src/conversationCarry.ts @@ -97,14 +97,45 @@ function truncateMessage(text: string, maxChars: number): string { return `${text.slice(0, maxChars - 1)}…`; } -const TRANSITIONAL_ASSISTANT = - /^(?:okay[,.]?\s+|ok[,.]?\s+|sure[,.]?\s+)?(?:let me|i(?:'ll| will)|now let me)\b/i; +/** Keep in sync with packages/v8 isTransitionalAssistantAnswer heuristics. */ +const TRANSITIONAL_OPENERS = + /^(?:okay[,.]?\s+|ok[,.]?\s+|sure[,.]?\s+|alright[,.]?\s+|right[,.]?\s+)?(?:let me|i(?:'ll| will)|i(?:'m| am) going to|now let me|next[,]? (?:i(?:'ll| will)|let me)|i need to|i should)\b/i; +const TRANSITIONAL_INTENT = + /\b(?:let me|i(?:'ll| will)|i(?:'m| am) going to)\b/i; +const TRANSITIONAL_CLOSERS = /(?::|\.\.\.|…)\s*$/; +const TRAILING_INTENT_CLAUSE = + /[.!,;]\s*(?:let me|i(?:'ll| will)|i(?:'m| am) going to)\b[\s\S]{0,160}$/i; function isWeakAssistantDisplay(text: string): boolean { const trimmed = text.trim(); if (trimmed.length === 0) return true; - if (trimmed.length < 220 && TRANSITIONAL_ASSISTANT.test(trimmed)) return true; if (/^(?:\([\w_]+\)|Error:)/.test(trimmed) && trimmed.length < 80) return true; + if (/^Completed workspace edits\b/i.test(trimmed) && trimmed.length < 260) return true; + if (/^Completed workspace edits\b[\s\S]*\bChanged files \(\d+\):/i.test(trimmed)) { + return true; + } + if (trimmed.length > 600) return false; + + const singleBeat = + trimmed.split(/\n+/).filter((line) => line.trim().length > 0).length <= 2; + if (!singleBeat) return false; + + if (TRANSITIONAL_OPENERS.test(trimmed) && TRANSITIONAL_CLOSERS.test(trimmed)) { + return true; + } + if ( + TRANSITIONAL_OPENERS.test(trimmed) && + trimmed.length < 180 && + !/[.!]["']?\s*$/.test(trimmed) + ) { + return true; + } + if (TRANSITIONAL_INTENT.test(trimmed) && TRANSITIONAL_CLOSERS.test(trimmed)) { + return true; + } + if (trimmed.length < 280 && TRAILING_INTENT_CLAUSE.test(trimmed)) { + return true; + } return false; } @@ -120,24 +151,16 @@ export function enrichAssistantCarryText(options: { const paths = [...(options.changedPaths ?? [])].filter( (path) => path.trim().length > 0, ); - const list = - paths.length > 0 - ? `${paths.slice(0, 40).join(', ')}${paths.length > 40 ? ', …' : ''}` - : ''; const incomplete = isWeakAssistantDisplay(answer); if (paths.length === 0) { return answer || '(no answer)'; } - const summary = `Changed files (${paths.length}): ${list}`; if (incomplete) { - return `Completed workspace edits.\n${summary}`; + return `Completed workspace edits (${paths.length} file${paths.length === 1 ? '' : 's'} changed).`; } - if (answer.includes('Changed files (')) { - return answer; - } - return `${answer}\n\n${summary}`; + return answer; } /** @@ -162,10 +185,6 @@ export function resolveDisplayedAssistantText(options: { if (!streamedStronger) return final; - const changedIdx = final.indexOf('Changed files ('); - if (changedIdx >= 0 && !streamed.includes('Changed files (')) { - return `${streamed}\n\n${final.slice(changedIdx).trim()}`; - } return streamed; } diff --git a/apps/vscode/src/hostAsk.ts b/apps/vscode/src/hostAsk.ts index ffe3755..d73f471 100644 --- a/apps/vscode/src/hostAsk.ts +++ b/apps/vscode/src/hostAsk.ts @@ -650,7 +650,7 @@ export async function runAskInOutputChannel(options: { (!hasPinnedContext || options.depth === 'deep'); if (includeRepoMap) { try { - const maxFiles = hasPinnedContext ? 80 : 400; + const maxFiles = hasPinnedContext ? 60 : 200; const snap = await buildWorkspaceSnapshot({ workspaceRoot, workspaceId: options.workspaceId ?? 'vscode_workspace', diff --git a/apps/vscode/src/pathSearch.ts b/apps/vscode/src/pathSearch.ts index 31169fb..c488bfa 100644 --- a/apps/vscode/src/pathSearch.ts +++ b/apps/vscode/src/pathSearch.ts @@ -1,4 +1,5 @@ -import { readdir, stat } from 'node:fs/promises'; +import type { Dirent } from 'node:fs'; +import { readdir } from 'node:fs/promises'; import { join, relative } from 'node:path'; import type { PathSuggestion } from './protocol.js'; @@ -12,6 +13,13 @@ const SKIP = new Set([ '.cursor', ]); +const CACHE_TTL_MS = 30_000; +const MAX_CATALOG_ENTRIES = 15_000; +const pathCache = new Map< + string, + { expiresAt: number; promise: Promise } +>(); + /** * Bounded workspace path search for @-mention autocomplete. */ @@ -21,42 +29,76 @@ export async function searchWorkspacePaths( limit = 24, ): Promise { const needle = query.trim().toLowerCase().replace(/^@/, ''); - const hits: PathSuggestion[] = []; + const catalog = await readCachedCatalog(workspaceRoot); + return catalog + .filter((item) => { + if (!needle) return true; + const path = item.path.toLowerCase(); + const name = path.split('/').pop() ?? path; + return path.includes(needle) || name.includes(needle); + }) + .sort((a, b) => scorePath(b, needle) - scorePath(a, needle)) + .slice(0, limit); +} + +async function readCachedCatalog( + workspaceRoot: string, +): Promise { + const now = Date.now(); + const cached = pathCache.get(workspaceRoot); + if (cached && cached.expiresAt > now) return cached.promise; + const promise = buildPathCatalog(workspaceRoot).catch((error) => { + pathCache.delete(workspaceRoot); + throw error; + }); + pathCache.set(workspaceRoot, { expiresAt: now + CACHE_TTL_MS, promise }); + return promise; +} + +async function buildPathCatalog( + workspaceRoot: string, +): Promise { + const entries: PathSuggestion[] = []; async function walk(dir: string): Promise { - if (hits.length >= limit) return; - let names: string[]; + if (entries.length >= MAX_CATALOG_ENTRIES) return; + let names: Dirent[]; try { - names = await readdir(dir); + names = await readdir(dir, { withFileTypes: true }); } catch { return; } - for (const name of names) { - if (hits.length >= limit) return; - if (SKIP.has(name)) continue; - const full = join(dir, name); - let info; - try { - info = await stat(full); - } catch { - continue; - } + for (const entry of names) { + if (entries.length >= MAX_CATALOG_ENTRIES) return; + if (SKIP.has(entry.name)) continue; + const full = join(dir, entry.name); const rel = relative(workspaceRoot, full).replace(/\\/g, '/'); - const match = - !needle || - rel.toLowerCase().includes(needle) || - name.toLowerCase().includes(needle); - if (info.isDirectory()) { - if (match) hits.push({ path: rel, kind: 'folder' }); + if (entry.isDirectory()) { + entries.push({ path: rel, kind: 'folder' }); await walk(full); - continue; - } - if (info.isFile() && match) { - hits.push({ path: rel, kind: 'file' }); + } else if (entry.isFile()) { + entries.push({ path: rel, kind: 'file' }); } } } await walk(workspaceRoot); - return hits.slice(0, limit); + return entries.sort((a, b) => { + const depth = a.path.split('/').length - b.path.split('/').length; + if (depth !== 0) return depth; + if (a.kind !== b.kind) return a.kind === 'folder' ? -1 : 1; + return a.path.localeCompare(b.path); + }); +} + +function scorePath(item: PathSuggestion, needle: string): number { + const path = item.path.toLowerCase(); + const name = path.split('/').pop() ?? path; + let score = item.kind === 'folder' ? 1 : 0; + if (!needle) return score - item.path.split('/').length * 0.1; + if (name === needle) score += 100; + if (name.startsWith(needle)) score += 60; + if (path.startsWith(needle)) score += 40; + if (path.includes(`/${needle}`)) score += 20; + return score - item.path.split('/').length * 0.2; } diff --git a/apps/vscode/src/runReport.ts b/apps/vscode/src/runReport.ts index 2c15406..57125ec 100644 --- a/apps/vscode/src/runReport.ts +++ b/apps/vscode/src/runReport.ts @@ -47,6 +47,15 @@ export function formatRunDiagnostics(result: AgentRunResult): string[] { ); } + if (codes.has('prompt_blocked') || result.error?.code === 'prompt_blocked') { + lines.push( + '[prompt] blocked — composed context exceeded the model input budget before the first model call.', + ); + lines.push( + '[hint] Use a larger context window, lower max output, disable extra context toggles, or pin fewer/shorter files.', + ); + } + return lines; } @@ -75,6 +84,14 @@ export function formatVisibleFailureDetails(options: { if (result.reasonCodes?.length) { lines.push(`Reason codes: ${result.reasonCodes.join(', ')}`); } + if ( + result.reasonCodes?.includes('prompt_blocked') || + result.error?.code === 'prompt_blocked' + ) { + lines.push( + 'Hint: prompt construction overflowed the input budget before any model/tool call ran.', + ); + } if (verification?.type === 'verification_completed') { lines.push( `Verification: ${verification.status} (${verification.reasonCodes.join(', ')})`, diff --git a/apps/vscode/src/sessionLog.ts b/apps/vscode/src/sessionLog.ts index f65016b..6d1fdbf 100644 --- a/apps/vscode/src/sessionLog.ts +++ b/apps/vscode/src/sessionLog.ts @@ -42,6 +42,23 @@ function writeLine(file: string, entry: unknown): void { appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8'); } +function compactText(text: string | undefined, maxChars = 4000): { + text?: string; + chars: number; + truncated: boolean; +} { + const value = text ?? ''; + if (!value) return { chars: 0, truncated: false }; + if (value.length <= maxChars) { + return { text: value, chars: value.length, truncated: false }; + } + return { + text: `${value.slice(0, maxChars)}…`, + chars: value.length, + truncated: true, + }; +} + function compactEvent(event: RunEvent): Record { const base: Record = { kind: 'event', @@ -63,7 +80,7 @@ function compactEvent(event: RunEvent): Record { return { ...base, deltaKind: event.kind, - preview: event.preview, + ...(event.kind === 'tool_call' ? { preview: event.preview } : {}), }; case 'tool_started': return { ...base, toolName: event.toolName, summary: event.summary }; @@ -109,11 +126,14 @@ function compactEvent(event: RunEvent): Record { warnings: event.warnings, }; case 'terminal': + const answer = compactText(event.result.answer, 1200); return { ...base, status: event.status, usage: event.result.usage, - answerChars: (event.result.answer ?? '').length, + answerChars: answer.chars, + answerTruncated: answer.truncated, + ...(answer.text ? { answerPreview: answer.text } : {}), error: event.result.error?.message, }; case 'state_pinned': @@ -166,14 +186,15 @@ export function appendSessionLog( }); for (const event of entry.events) { - // Skip per-token reasoning previews that only duplicate the stream; - // keep content + tool_call deltas and all non-delta events. - if (event.type === 'model_delta' && event.kind === 'reasoning') { + // Skip per-token text/reasoning previews that duplicate the final answer + // and make logs unreadable; keep tool-call deltas and structured events. + if (event.type === 'model_delta' && event.kind !== 'tool_call') { continue; } writeLine(file, compactEvent(event)); } + const answer = compactText(entry.result.answer); writeLine(file, { kind: 'run_end', at: new Date().toISOString(), @@ -182,7 +203,9 @@ export function appendSessionLog( route: entry.result.route, usage: entry.result.usage, durationMs: entry.result.durationMs, - answer: entry.result.answer, + answerChars: answer.chars, + answerTruncated: answer.truncated, + ...(answer.text ? { answer: answer.text } : {}), error: entry.result.error, reasonCodes: entry.result.reasonCodes, }); diff --git a/apps/vscode/src/sidebar.ts b/apps/vscode/src/sidebar.ts index a5d5fe1..4983f87 100644 --- a/apps/vscode/src/sidebar.ts +++ b/apps/vscode/src/sidebar.ts @@ -420,10 +420,9 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider { if (!message || typeof message !== 'object') return; switch (message.type) { case 'ready': { - // Post a quick bootstrap first, then index so the UI is not blank. + // Post a quick bootstrap first, then continue index work in the background. await this.sendBootstrap(); - const indexed = await this.ensureIndexed(); - this.post({ type: 'index.status', index: indexed }); + this.startBackgroundIndex('initial load'); return; } case 'ask': @@ -730,14 +729,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider { this.post({ type: 'index.status', index: await this.readIndexStatus() }); return; case 'index.reindex': { - this.post({ - type: 'index.status', - index: { - ...this.lastIndex, - message: 'Indexing workspace…', - readiness: 'indexing', - }, - }); + this.postIndexingStatus('Indexing workspace…'); const index = await this.onIndexWorkspace(); this.lastIndex = index; this.post({ type: 'index.status', index }); @@ -1392,6 +1384,50 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider { }; } + private startBackgroundIndex(reason: string): void { + const root = this.effectiveRoot(); + if (!root) { + this.post({ + type: 'index.status', + index: { + fileCount: 0, + truncated: false, + message: 'Open a workspace folder to index', + }, + }); + return; + } + this.postIndexingStatus('Checking repository index…'); + void this.ensureIndexed() + .then((index) => { + this.lastIndex = index; + this.post({ type: 'index.status', index }); + this.channel.appendLine( + `[index] ${reason} ${index.message ?? 'ready'} files=${index.fileCount}`, + ); + }) + .catch((error) => { + const message = + error instanceof Error ? error.message : String(error); + this.lastIndex = { + ...this.lastIndex, + readiness: 'unavailable', + message: `Index failed: ${message}`, + }; + this.post({ type: 'index.status', index: this.lastIndex }); + this.channel.appendLine(`[index] ${reason} failed: ${message}`); + }); + } + + private postIndexingStatus(message: string): void { + this.lastIndex = { + ...this.lastIndex, + message, + readiness: 'indexing', + }; + this.post({ type: 'index.status', index: this.lastIndex }); + } + private async handleTestConnection( message: Extract, ): Promise { @@ -1852,14 +1888,7 @@ export class MitiiSidebarProvider implements vscode.WebviewViewProvider { } this.channel.appendLine('[index] first load: publishing host snapshot…'); - this.post({ - type: 'index.status', - index: { - fileCount: 0, - truncated: false, - message: 'Indexing workspace…', - }, - }); + this.postIndexingStatus('Indexing workspace…'); const status = await this.publishIndexSnapshot(); this.channel.appendLine( `[index] first-load ${status.message ?? 'done'} readiness=${status.readiness ?? 'n/a'}`, diff --git a/apps/vscode/webview-ui/src/App.tsx b/apps/vscode/webview-ui/src/App.tsx index 6673602..6f86fa3 100644 --- a/apps/vscode/webview-ui/src/App.tsx +++ b/apps/vscode/webview-ui/src/App.tsx @@ -11,10 +11,6 @@ import { import { onHostMessage, postToHost } from './bridge'; import { ContextPanel, type ContextPin } from './components/ContextPanel'; import { ErrorBanner } from './components/ErrorBanner'; -import { - FileChangesBar, - FileChangesCard, -} from './components/FileChangesCard'; import { HistoryPanel } from './components/HistoryPanel'; import { IconButton } from './components/IconButton'; import { @@ -187,15 +183,12 @@ export function App() { const [depth, setDepth] = useState('auto'); const [prompt, setPrompt] = useState(''); const [pinned, setPinned] = useState([]); - const [fileChanges, setFileChanges] = useState( - null, - ); - const [fileChangesBarExpanded, setFileChangesBarExpanded] = useState(false); const [turns, setTurns] = useState([]); const [running, setRunning] = useState(false); const [error, setError] = useState(null); const [suggestions, setSuggestions] = useState([]); const [suggestOpen, setSuggestOpen] = useState(false); + const [suggestLoading, setSuggestLoading] = useState(false); const [suggestQuery, setSuggestQuery] = useState(''); const [activeSuggest, setActiveSuggest] = useState(0); const [workspace, setWorkspace] = useState({}); @@ -369,8 +362,6 @@ export function App() { setRunning(true); setError(null); setPlan(null); - setFileChanges(null); - setFileChangesBarExpanded(false); stickToBottomRef.current = true; forceScrollToBottomRef.current = true; const userId = uid('user'); @@ -496,7 +487,8 @@ export function App() { case 'paths.results': if (msg.requestId === lastSearchId.current) { setSuggestions(msg.suggestions); - setSuggestOpen(msg.suggestions.length > 0); + setSuggestLoading(false); + setSuggestOpen(true); setActiveSuggest(0); } break; @@ -545,22 +537,21 @@ export function App() { break; } case 'run.fileChanges': { - setFileChanges(msg.changes); - setFileChangesBarExpanded(false); const id = activeAssistantId.current; - if (id) { - setTurns((prev) => - prev.map((t) => - t.id === id ? { ...t, fileChanges: msg.changes } : t, - ), + setTurns((prev) => { + const targetId = + id ?? + [...prev] + .reverse() + .find((turn) => turn.role === 'assistant')?.id; + if (!targetId) return prev; + return prev.map((t) => + t.id === targetId ? { ...t, fileChanges: msg.changes } : t, ); - } + }); break; } case 'fileChanges.undone': { - setFileChanges((prev) => - prev?.runId === msg.runId ? null : prev, - ); setTurns((prev) => prev.map((t) => t.fileChanges?.runId === msg.runId @@ -586,10 +577,6 @@ export function App() { contextWindow: provider.contextWindow || 32768, }); setTurns(msg.messages.map((m) => toChatTurn(m))); - setFileChanges( - [...msg.messages].reverse().find((m) => m.fileChanges)?.fileChanges ?? - null, - ); setNav('chat'); break; case 'setPlan': @@ -681,6 +668,7 @@ export function App() { pinnedPaths: pinned.map((p) => p.path), }); setPrompt(''); + setSuggestLoading(false); setSuggestOpen(false); }, [prompt, running, mode, depth, pinned]); @@ -692,8 +680,11 @@ export function App() { setSuggestQuery(q); const requestId = String(++searchReq.current); lastSearchId.current = requestId; + setSuggestLoading(true); + setSuggestOpen(true); postToHost({ type: 'paths.search', query: q, requestId }); } else { + setSuggestLoading(false); setSuggestOpen(false); } }; @@ -712,6 +703,7 @@ export function App() { } return [...prev, { path, source: 'user' }]; }); + setSuggestLoading(false); setSuggestOpen(false); }; @@ -744,7 +736,6 @@ export function App() { const dismissFileChanges = useCallback((runId: string) => { postToHost({ type: 'dismissFileChanges', runId }); - setFileChanges((prev) => (prev?.runId === runId ? null : prev)); setTurns((prev) => prev.map((t) => t.fileChanges?.runId === runId @@ -972,31 +963,6 @@ export function App() { ) : (
- {fileChanges ? ( - setFileChangesBarExpanded(true)} - onUndo={() => undoFileChanges(fileChanges.runId)} - onReviewAll={() => reviewAllFileChanges(fileChanges)} - /> - ) : null} - {fileChangesBarExpanded && fileChanges ? ( -
- openFile(path)} - onReviewFile={(path) => - reviewFileChange(fileChanges.runId, path) - } - onUndo={() => undoFileChanges(fileChanges.runId)} - onReviewAll={() => reviewAllFileChanges(fileChanges)} - onDismiss={() => { - setFileChangesBarExpanded(false); - dismissFileChanges(fileChanges.runId); - }} - /> -
- ) : null} {suggestOpen ? (
- {suggestions.map((s, i) => ( + {suggestLoading ? ( +
+ Loading files… +
+ ) : null} + {!suggestLoading && suggestions.length === 0 ? ( +
+ No matching files +
+ ) : null} + {!suggestLoading ? suggestions.map((s, i) => ( - ))} + )) : null}
) : null}