diff --git a/.changeset/clear-history-navigation.md b/.changeset/clear-history-navigation.md new file mode 100644 index 00000000000..4edf1ee7dcf --- /dev/null +++ b/.changeset/clear-history-navigation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Correct screen reader and keyboard operation for session history rows and Local or Cloud history navigation. diff --git a/.changeset/clear-model-navigation.md b/.changeset/clear-model-navigation.md new file mode 100644 index 00000000000..932636c1a70 --- /dev/null +++ b/.changeset/clear-model-navigation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Make model selection in chat and settings operable with screen readers by announcing searchable options, keyboard navigation, selected values, and model-setting purpose. diff --git a/.changeset/fix-lancedb-metadata-type-coercion.md b/.changeset/fix-lancedb-metadata-type-coercion.md new file mode 100644 index 00000000000..42172ed84a3 --- /dev/null +++ b/.changeset/fix-lancedb-metadata-type-coercion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix LanceDB metadata corruption that caused a full re-index on every VS Code restart diff --git a/.changeset/forked-session-context.md b/.changeset/forked-session-context.md new file mode 100644 index 00000000000..b3061ce95e7 --- /dev/null +++ b/.changeset/forked-session-context.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Preserve prior context in forked sessions while recognizing the selected direction and current worktree context. diff --git a/.changeset/gateway-byok-footer-link.md b/.changeset/gateway-byok-footer-link.md new file mode 100644 index 00000000000..8f63766d93b --- /dev/null +++ b/.changeset/gateway-byok-footer-link.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Link to Kilo Gateway BYOK usage information from provider API key connection dialogs. diff --git a/.changeset/recall-sibling-context.md b/.changeset/recall-sibling-context.md new file mode 100644 index 00000000000..3ecc1fb6f3d --- /dev/null +++ b/.changeset/recall-sibling-context.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Guide Agent Manager orchestration to recall completed session context only when needed. diff --git a/.changeset/visible-semantic-indexing.md b/.changeset/visible-semantic-indexing.md new file mode 100644 index 00000000000..b7e258038a6 --- /dev/null +++ b/.changeset/visible-semantic-indexing.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Access semantic indexing without an experimental feature toggle while keeping indexing disabled until enabled globally or for a project. diff --git a/.changeset/webview-accessibility-validation.md b/.changeset/webview-accessibility-validation.md new file mode 100644 index 00000000000..194f77bd216 --- /dev/null +++ b/.changeset/webview-accessibility-validation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Support accessibility regression checks and assistive-technology testing for VS Code webviews. diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml index d059175adc8..6717c0fff59 100644 --- a/.github/workflows/visual-regression.yml +++ b/.github/workflows/visual-regression.yml @@ -26,6 +26,7 @@ jobs: - "packages/kilo-vscode/.storybook/**" - "packages/kilo-vscode/tests/visual-regression*" - "packages/kilo-vscode/tests/permission-dock-dropdown*" + - "packages/kilo-vscode/tests/accessibility*" # kilocode_change - "packages/kilo-docs/public/img/screenshot-tests/**" - ".github/workflows/visual-regression.yml" - name: Check if PR is from a fork @@ -277,7 +278,7 @@ jobs: run: bun run build-storybook working-directory: packages/kilo-vscode - - name: Generate baselines for new/missing stories + - name: Generate baselines and enforce webview accessibility checks # kilocode_change run: bun run test:visual:update working-directory: packages/kilo-vscode env: diff --git a/bun.lock b/bun.lock index bb84573376d..1abda6e1a39 100644 --- a/bun.lock +++ b/bun.lock @@ -266,7 +266,9 @@ "zod": "^3.24.2", }, "devDependencies": { + "@axe-core/playwright": "4.11.3", "@playwright/test": "1.57.0", + "@storybook/addon-a11y": "10.2.10", "@storybook/addon-docs": "10.2.10", "@types/diff": "^6.0.0", "@types/mocha": "^10.0.10", @@ -832,6 +834,8 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@axe-core/playwright": ["@axe-core/playwright@4.11.3", "", { "dependencies": { "axe-core": "~4.11.4" }, "peerDependencies": { "playwright-core": ">= 1.0.0" } }, "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w=="], + "@azu/format-text": ["@azu/format-text@1.0.2", "", {}, "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg=="], "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], @@ -4444,6 +4448,8 @@ "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + "@axe-core/playwright/axe-core": ["axe-core@4.11.4", "", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], + "@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], diff --git a/packages/kilo-docs/pages/customize/agents-md.md b/packages/kilo-docs/pages/customize/agents-md.md index 7eccef968da..cbe0e6743fb 100644 --- a/packages/kilo-docs/pages/customize/agents-md.md +++ b/packages/kilo-docs/pages/customize/agents-md.md @@ -57,21 +57,25 @@ my-project/ The filename must be uppercase (`AGENTS.md`), not lowercase (`agents.md`). This ensures consistency across different operating systems and tools. {% /callout %} -### Subdirectory AGENTS.md Files +### Per-Directory AGENTS.md Files -You can also place AGENTS.md files in subdirectories to provide context-specific instructions: +You can place AGENTS.md files in subdirectories to provide context-specific instructions when the agent accesses files in those locations: ``` my-project/ ├── AGENTS.md # Root-level instructions ├── src/ │ └── backend/ -│ └── AGENTS.md # Backend-specific instructions +│ └── AGENTS.md # Backend-specific instructions (loaded when reading backend files) └── docs/ - └── AGENTS.md # Documentation-specific instructions + └── AGENTS.md # Documentation-specific instructions (loaded when reading docs files) ``` -When working in a subdirectory, Kilo Code will load both the root AGENTS.md and any subdirectory AGENTS.md files, with subdirectory files taking precedence for conflicting instructions. +{% callout type="info" %} +Per-directory AGENTS.md files are **dynamically loaded** when the agent reads files in that directory - they are not pre-loaded at session start. When the agent reads a file in `src/backend/`, the corresponding `AGENTS.md` is discovered and its contents are injected into the conversation as `` tags. + +This is useful for providing context-specific guidance for different parts of a monorepo or project. +{% /callout %} ## File Protection diff --git a/packages/kilo-docs/pages/customize/context/codebase-indexing.md b/packages/kilo-docs/pages/customize/context/codebase-indexing.md index f577678670b..819ae399d7b 100644 --- a/packages/kilo-docs/pages/customize/context/codebase-indexing.md +++ b/packages/kilo-docs/pages/customize/context/codebase-indexing.md @@ -7,8 +7,8 @@ description: "Index your codebase for improved AI understanding" Codebase Indexing enables semantic code search across your entire project using AI embeddings. Instead of searching for exact text matches, it understands the _meaning_ of your queries, helping Kilo Code find relevant code even when you don't know specific function names or file locations. -{% callout type="warning" title="Experimental" %} -Codebase Indexing is currently **experimental** in the CLI and the new VS Code extension. You must explicitly opt in before the feature becomes available — see the **Setup** section below. Behavior, configuration, and defaults may change in future releases. +{% callout type="info" title="Opt-in indexing" %} +Codebase Indexing is disabled by default. It starts only after you enable indexing globally or for an individual project. Configuring an embedding provider without enabling one of those toggles does not start indexing. {% /callout %} ## What It Does @@ -34,28 +34,10 @@ This enables natural language queries like "user authentication logic" or "datab {% tabs %} {% tab label="VSCode" %} -### 1. Enable the experimental flag - -Codebase Indexing is gated behind an experimental flag. Until the flag is on, the Indexing UI is hidden and `semantic_search` is unavailable. - -1. Open Kilo Code **Settings** → **Experimental**. -2. Toggle **Semantic Indexing** on. -3. The **Indexing** tab will appear in Settings and the indexing status indicator will appear at the bottom of the prompt input panel. - -Alternatively, set `experimental.semantic_indexing` to `true` in your `kilo.jsonc`: - -```json -{ - "experimental": { - "semantic_indexing": true - } -} -``` - -### 2. Configure indexing +### Configure indexing 1. Open Kilo Code **Settings** → **Indexing**, or click the indexing indicator at the bottom of the prompt input panel. -2. Toggle **Enable Indexing** on. +2. Turn on **Global Enable** to index every workspace, or turn on **Enable for This Project** to index only the current workspace. Both toggles are off until explicitly enabled. 3. Pick an **Embedding Provider** and fill in its required fields. 4. Pick a **Vector Store** (`Qdrant` or `LanceDB`) and configure it. 5. Optionally adjust **Tuning Parameters** (search score, batch size, retries, max results). @@ -106,23 +88,9 @@ The prompt input panel shows a compact indexing status indicator that reflects t {% /tab %} {% tab label="CLI" %} -### 1. Enable the experimental flag +### Configure indexing -Codebase Indexing is gated behind an experimental flag. Until the flag is on, the `/indexing` command is hidden and `semantic_search` is unavailable. - -Set the flag in your `kilo.jsonc`: - -```json -{ - "experimental": { - "semantic_indexing": true - } -} -``` - -Restart the CLI for the change to take effect. The `/indexing` command (and aliases `/index`, `/embedding`) will appear in the command palette once the flag is active. - -### 2. Configure indexing +The `/indexing` command (and aliases `/index`, `/embedding`) is available when the indexing plugin is installed. Indexing remains disabled until it is enabled globally or for the current project. Open a Kilo TUI session and run: @@ -198,7 +166,7 @@ When indexing is enabled, the CLI shows an indexing status badge at the bottom o {% /tab %} {% tab label="VSCode (Legacy)" %} -The legacy extension does not require an experimental flag. +The legacy extension uses its own Codebase Indexing settings panel. ### Open Codebase Indexing Settings diff --git a/packages/kilo-docs/pages/customize/custom-instructions.md b/packages/kilo-docs/pages/customize/custom-instructions.md index c46dda3120e..4caf933f583 100644 --- a/packages/kilo-docs/pages/customize/custom-instructions.md +++ b/packages/kilo-docs/pages/customize/custom-instructions.md @@ -50,7 +50,7 @@ Project-level instructions are loaded before global instructions and apply to ev You can place `AGENTS.md` files in any subdirectory of your project. These are loaded dynamically — when the agent's Read tool accesses a file in that directory, the corresponding `AGENTS.md` is discovered and its contents are injected into the conversation as `` tags. -This is useful for providing context-specific guidance for different parts of a monorepo or project. +This is useful for providing context-specific guidance for different parts of a monorepo or project. The subdirectory file does not need to duplicate root-level instructions; it supplements them for tasks within that directory. ## Additional Instruction Sources @@ -127,7 +127,7 @@ Project-level instructions are loaded before global instructions and apply to ev You can place `AGENTS.md` files in any subdirectory of your project. These are loaded dynamically — when the agent's Read tool accesses a file in that directory, the corresponding `AGENTS.md` is discovered and its contents are injected into the conversation as `` tags. -This is useful for providing context-specific guidance for different parts of a monorepo or project. +This is useful for providing context-specific guidance for different parts of a monorepo or project. The subdirectory file does not need to duplicate root-level instructions; it supplements them for tasks within that directory. ## Additional Instruction Sources diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/sources-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/sources-chromium-linux.png new file mode 100644 index 00000000000..d64935ee798 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/sources-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4caad41e47c02636156c91066ef88dc4cb0df298ef1a0f2048cad12aafd985ee +size 21865 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png new file mode 100644 index 00000000000..01ea6e7ac02 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-accessible-labels-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbfed6606c5e1ccd6a744bdadf34ee597007d5f82c436c8c53e189560e0837d4 +size 30244 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png new file mode 100644 index 00000000000..f6894817821 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-accessible-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838 +size 1085 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png new file mode 100644 index 00000000000..f6894817821 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-selected-favorite-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838 +size 1085 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index e4b29c05385..a7f098288a0 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -35,6 +35,8 @@ - +- + - - diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 87f1c525f82..265a1264835 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "قم بزيارة ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " للحصول على مفتاح API الخاص بك.", + "provider.connect.kiloGateway.byok.prefix": "للحصول على المزيد من إحصائيات الاستخدام، استخدم ", + "provider.connect.kiloGateway.byok.link": "BYOK عبر Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "موصى به", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 21864df9338..54198063b33 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Visite ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " para obter sua chave de API.", + "provider.connect.kiloGateway.byok.prefix": "Para mais estatísticas de uso, utilize ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Recomendados", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index e517c1352f2..b5de90e2493 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Posjetite ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " da preuzmete svoj API ključ.", + "provider.connect.kiloGateway.byok.prefix": "Za više statistika korištenja, koristite ", + "provider.connect.kiloGateway.byok.link": "BYOK putem Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Preporučeno", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index b8189401bd6..0f52fcf19be 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Besøg ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " for at hente din API-nøgle.", + "provider.connect.kiloGateway.byok.prefix": "For flere brugsstatistikker, brug ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Anbefalede", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index a65837305ff..ade7e97a636 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Besuchen Sie ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.", + "provider.connect.kiloGateway.byok.prefix": "Für weitere Nutzungsstatistiken ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": " nutzen.", // Provider dialog translations "dialog.provider.group.recommended": "Empfohlen", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index e2bc7de26d5..3aae15ff32a 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -9,6 +9,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Visit ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " to collect your API key.", + "provider.connect.kiloGateway.byok.prefix": "For more usage stats, ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Recommended", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 8312118c26a..bf7eeb3e48e 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Visita ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " para obtener tu clave API.", + "provider.connect.kiloGateway.byok.prefix": "Para más estadísticas de uso, utiliza ", + "provider.connect.kiloGateway.byok.link": "BYOK a través de Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Recomendados", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index 510c0c7df91..d7c24995a89 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Visitez ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " pour obtenir votre clé API.", + "provider.connect.kiloGateway.byok.prefix": "Pour plus de statistiques d'utilisation, utilisez ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Recommandés", diff --git a/packages/kilo-i18n/src/it.ts b/packages/kilo-i18n/src/it.ts index 3c0fca27ddc..b2d80ed24a6 100644 --- a/packages/kilo-i18n/src/it.ts +++ b/packages/kilo-i18n/src/it.ts @@ -9,6 +9,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Visita ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " per ottenere la tua API key.", + "provider.connect.kiloGateway.byok.prefix": "Per ulteriori statistiche sull'utilizzo, utilizza ", + "provider.connect.kiloGateway.byok.link": "BYOK tramite Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Consigliati", @@ -42,8 +45,7 @@ export const dict = { "marketplace.install.done": "Fatto", "marketplace.install.close": "Chiudi", "marketplace.remove.title": "Rimuovere {{name}}?", - "marketplace.remove.confirm": - "Vuoi davvero rimuovere questo {{type}}? Verrà rimosso dalla configurazione {{scope}}.", + "marketplace.remove.confirm": "Vuoi davvero rimuovere questo {{type}}? Verrà rimosso dalla configurazione {{scope}}.", "marketplace.remove.cancel": "Annulla", "marketplace.remove.confirm.button": "Rimuovi", "marketplace.tab.mcp": "MCP", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 634e4729604..a570e5defc2 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -6,6 +6,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " にアクセスしてAPIキーを取得してください。", + "provider.connect.kiloGateway.byok.prefix": "詳細な使用統計については、", + "provider.connect.kiloGateway.byok.link": "Kilo's Gateway経由でBYOK", + "provider.connect.kiloGateway.byok.suffix": "をご利用ください。", // Provider dialog translations "dialog.provider.group.recommended": "おすすめ", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 1e3c37ee0f5..843eeba2d33 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -6,6 +6,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": "를 방문하여 API 키를 받으세요.", + "provider.connect.kiloGateway.byok.prefix": "더 많은 사용 통계를 보려면 ", + "provider.connect.kiloGateway.byok.link": "Kilo's Gateway를 통해 BYOK", + "provider.connect.kiloGateway.byok.suffix": "를 사용하세요.", // Provider dialog translations "dialog.provider.group.recommended": "추천", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 3e42fb7abff..8c6d82c315d 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -9,6 +9,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Bezoek ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " om je API key op te halen.", + "provider.connect.kiloGateway.byok.prefix": "Voor meer gebruiksstatistieken, gebruik ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Aanbevolen", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index c23bf6c3e84..e680849c09e 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Besøk ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " for å hente API-nøkkelen din.", + "provider.connect.kiloGateway.byok.prefix": "For mer bruksstatistikk, bruk ", + "provider.connect.kiloGateway.byok.link": "BYOK via Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Anbefalt", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 09cbe56e59b..259a273b3d3 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Odwiedź ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": ", aby otrzymać swój klucz API.", + "provider.connect.kiloGateway.byok.prefix": "Aby uzyskać więcej statystyk użycia, użyj ", + "provider.connect.kiloGateway.byok.link": "BYOK przez Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Polecane", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 89dc4e409b6..faa2fca6228 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Посетите ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": ", чтобы получить ваш API-ключ.", + "provider.connect.kiloGateway.byok.prefix": "Для получения дополнительной статистики использования используйте ", + "provider.connect.kiloGateway.byok.link": "BYOK через Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Рекомендуемые", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 8affeb54c16..29060784760 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "เยี่ยมชม ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " เพื่อรับ API key ของคุณ", + "provider.connect.kiloGateway.byok.prefix": "สำหรับสถิติการใช้งานเพิ่มเติม โปรดใช้ ", + "provider.connect.kiloGateway.byok.link": "BYOK ผ่าน Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": "", // Provider dialog translations "dialog.provider.group.recommended": "แนะนำ", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index b4e335f0501..1ec28fe0eee 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "API anahtarınızı almak için ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " adresini ziyaret edin.", + "provider.connect.kiloGateway.byok.prefix": "Daha fazla kullanım istatistiği için ", + "provider.connect.kiloGateway.byok.link": "Kilo's Gateway üzerinden BYOK", + "provider.connect.kiloGateway.byok.suffix": " kullanın.", // Provider dialog translations "dialog.provider.group.recommended": "Önerilen", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index 795fd1398bc..afe7ee3cfd5 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -7,6 +7,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "Відвідайте ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " щоб отримати свій API-ключ.", + "provider.connect.kiloGateway.byok.prefix": "Для отримання додаткової статистики використання використовуйте ", + "provider.connect.kiloGateway.byok.link": "BYOK через Kilo's Gateway", + "provider.connect.kiloGateway.byok.suffix": ".", // Provider dialog translations "dialog.provider.group.recommended": "Рекомендовані", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index f23fbb9c66a..3096ab5dd5f 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -5,6 +5,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "访问 ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " 获取您的 API 密钥。", + "provider.connect.kiloGateway.byok.prefix": "如需更多使用统计信息,请", + "provider.connect.kiloGateway.byok.link": "通过 Kilo's Gateway 进行 BYOK", + "provider.connect.kiloGateway.byok.suffix": "。", // Provider dialog translations "dialog.provider.group.recommended": "推荐", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 930ddcf3ac5..8e0ee47746b 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -5,6 +5,9 @@ export const dict = { "provider.connect.kiloGateway.visit.prefix": "訪問 ", "provider.connect.kiloGateway.visit.link": "kilo.ai", "provider.connect.kiloGateway.visit.suffix": " 獲取您的 API 金鑰。", + "provider.connect.kiloGateway.byok.prefix": "如需更多使用統計資訊,請", + "provider.connect.kiloGateway.byok.link": "透過 Kilo's Gateway 進行 BYOK", + "provider.connect.kiloGateway.byok.suffix": "。", // Provider dialog translations "dialog.provider.group.recommended": "推薦", diff --git a/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts b/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts index 39bc14760dc..fc607371f9c 100644 --- a/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts +++ b/packages/kilo-indexing/src/indexing/vector-store/lancedb-vector-store.ts @@ -136,7 +136,7 @@ export class LanceDBVectorStore implements IVectorStore { return [ { key: KEY.size, - value: this.vectorSize, + value: String(this.vectorSize), }, { key: KEY.provider, @@ -148,11 +148,11 @@ export class LanceDBVectorStore implements IVectorStore { }, { key: KEY.dimension, - value: this.profile.dimension, + value: String(this.profile.dimension), }, { key: KEY.complete, - value: false, + value: "false", }, ] } @@ -572,7 +572,7 @@ export class LanceDBVectorStore implements IVectorStore { } const metadataTable = await db.openTable(this.metadataTableName) const metadataResults = await metadataTable.query().where(`key = '${KEY.complete}'`).toArray() - const indexed = metadataResults.length > 0 ? metadataResults[0].value : false + const indexed = metadataResults.length > 0 ? String(metadataResults[0].value) === "true" : false log.info("LanceDB indexing metadata evaluated", { workspacePath: this.workspacePath, pointCount, @@ -590,7 +590,9 @@ export class LanceDBVectorStore implements IVectorStore { throw new Error(`Invalid metadata key: ${key}`) } await metadataTable.delete(`key = '${key}'`) - await metadataTable.add([{ key, value }]) + // All values must be strings to prevent LanceDB from inferring the value column + // type as number from the first row, which corrupts subsequent string/boolean values. + await metadataTable.add([{ key, value: String(value) }]) } private async _persistEmbeddingProfile(metadataTable: Table): Promise { @@ -609,7 +611,7 @@ export class LanceDBVectorStore implements IVectorStore { const db = await this.getDb() const metadataTable = await db.openTable(this.metadataTableName) await this._persistEmbeddingProfile(metadataTable) - await this._upsertMetadata(metadataTable, KEY.complete, true) + await this._upsertMetadata(metadataTable, KEY.complete, "true") log.info("Marked indexing as complete") } catch (error) { log.error("Failed to mark indexing as complete", { error }) @@ -626,7 +628,7 @@ export class LanceDBVectorStore implements IVectorStore { const db = await this.getDb() const metadataTable = await db.openTable(this.metadataTableName) await this._persistEmbeddingProfile(metadataTable) - await this._upsertMetadata(metadataTable, KEY.complete, false) + await this._upsertMetadata(metadataTable, KEY.complete, "false") log.info("Marked indexing as incomplete (in progress)") } catch (error) { log.error("Failed to mark indexing as incomplete", { error }) diff --git a/packages/kilo-vscode/.storybook/main.ts b/packages/kilo-vscode/.storybook/main.ts index d1ef464f284..300333356fb 100644 --- a/packages/kilo-vscode/.storybook/main.ts +++ b/packages/kilo-vscode/.storybook/main.ts @@ -5,7 +5,7 @@ import solidPlugin from "vite-plugin-solid" const config: StorybookConfig = { framework: "storybook-solidjs-vite", stories: ["../webview-ui/src/stories/**/*.stories.@(ts|tsx)"], - addons: ["@storybook/addon-docs"], + addons: ["@storybook/addon-docs", "@storybook/addon-a11y"], staticDirs: [{ from: "../assets/icons", to: "/icons" }], refs: {}, viteFinal: async (config) => { diff --git a/packages/kilo-vscode/.storybook/preview.tsx b/packages/kilo-vscode/.storybook/preview.tsx index 21758200fda..7add8f8897e 100644 --- a/packages/kilo-vscode/.storybook/preview.tsx +++ b/packages/kilo-vscode/.storybook/preview.tsx @@ -82,6 +82,7 @@ const preview: Preview = { theme: "kilo-vscode", colorScheme: "dark", vscodeTheme: "dark-modern", + a11y: { manual: true }, }, } diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index e0475c4e79f..df4b569feb2 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -1017,6 +1017,7 @@ "rebuild-sdk": "bun run --cwd ../sdk/js build", "storybook": "storybook dev -p 6007", "build-storybook": "storybook build -o storybook-static", + "test:a11y": "playwright test tests/accessibility.spec.ts", "test:visual": "playwright test", "test:visual:update": "playwright test --update-snapshots", "snapshot:build": "bun script/dev-snapshot.ts build", @@ -1024,7 +1025,9 @@ "extension": "bun script/launch.ts" }, "devDependencies": { + "@axe-core/playwright": "4.11.3", "@playwright/test": "1.57.0", + "@storybook/addon-a11y": "10.2.10", "@storybook/addon-docs": "10.2.10", "@types/diff": "^6.0.0", "@types/mocha": "^10.0.10", diff --git a/packages/kilo-vscode/script/launch.ts b/packages/kilo-vscode/script/launch.ts index caa0a8b4936..464597bbc8d 100644 --- a/packages/kilo-vscode/script/launch.ts +++ b/packages/kilo-vscode/script/launch.ts @@ -14,6 +14,7 @@ * --wait Block until the VS Code window is closed * --clean Wipe the user-data and extensions dirs before launching * --preserve-settings Merge defaults into existing VS Code user settings + * --accessible Enable VS Code accessibility support for assistive-technology testing * * Environment: * VSCODE_EXEC_PATH Path to VS Code executable (same as --app-path) @@ -86,6 +87,7 @@ const explicit = opts["app-path"] as string | undefined const blocking = opts["wait"] === true const clean = opts["clean"] === true const preserve = opts["preserve-settings"] === true +const accessible = opts["accessible"] === true // --------------------------------------------------------------------------- // VS Code executable detection @@ -251,11 +253,11 @@ async function installVsix(path: string, app: string) { // Settings for isolated instance // --------------------------------------------------------------------------- -function settings(keep: boolean) { +function settings(keep: boolean, enabled: boolean) { const dir = join(userDir, "User") const file = join(dir, "settings.json") const defaults = { - "editor.accessibilitySupport": "off", + "editor.accessibilitySupport": enabled ? "on" : "off", "extensions.autoCheckUpdates": false, "extensions.autoUpdate": false, "extensions.ignoreRecommendations": true, @@ -269,7 +271,10 @@ function settings(keep: boolean) { } mkdirSync(dir, { recursive: true }) - const cfg = keep && existsSync(file) ? { ...defaults, ...load(file) } : defaults + const cfg = + keep && existsSync(file) + ? { ...defaults, ...load(file), ...(enabled ? { "editor.accessibilitySupport": "on" } : {}) } + : defaults writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n") } @@ -306,7 +311,7 @@ async function launch() { const app = detect() - settings(preserve) + settings(preserve, accessible) const args = [workspace, `--extensions-dir=${extDir}`, `--user-data-dir=${userDir}`, "--skip-release-notes"] @@ -338,6 +343,7 @@ async function launch() { console.log(`[launch] Executable: ${app}`) console.log(`[launch] Workspace: ${workspace}`) console.log(`[launch] State: ${base}`) + console.log(`[launch] Accessibility support: ${accessible ? "on" : "off"}`) if (blocking) { const result = Bun.spawnSync([app, ...args], { diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index d14dcbaf8ec..dd16ecc6098 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -1152,6 +1152,7 @@ export class AgentManagerProvider implements Disposable { { getClient: () => this.connectionService.getClient(), state: this.getStateManager(), + directory: this.getRoot(), postError: (msg) => this.postToWebview({ type: "error", message: msg }), registerWorktreeSession: (sid, dir) => this.registerWorktreeSession(sid, dir), pushState: () => this.pushState(), diff --git a/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts b/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts index e25cb355588..cd49f2d2064 100644 --- a/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts +++ b/packages/kilo-vscode/src/agent-manager/continue-in-worktree.ts @@ -4,6 +4,7 @@ import type { WorktreeStateManager } from "./WorktreeStateManager" import { capture as captureGitState, apply as applyGitState, type GitSnapshot } from "./git-transfer" import { getErrorMessage } from "../kilo-provider-utils" import { PLATFORM } from "./constants" +import { recordForkHandoff } from "./fork-handoff" export interface ContinueContext { root: string @@ -82,6 +83,9 @@ export async function forkSession(ctx: ContinueContext, sessionId: string, dir: } try { const { data } = await client.session.fork({ sessionID: sessionId, directory: dir }, { throwOnError: true }) + await recordForkHandoff({ client, sessionId: data.id, directory: dir }).catch((err) => { + ctx.log("Failed to record fork handoff:", getErrorMessage(err)) + }) return { ok: true, value: data } } catch (err) { return { ok: false, error: `Failed to fork session: ${getErrorMessage(err)}` } diff --git a/packages/kilo-vscode/src/agent-manager/fork-handoff.ts b/packages/kilo-vscode/src/agent-manager/fork-handoff.ts new file mode 100644 index 00000000000..efbf36361c9 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/fork-handoff.ts @@ -0,0 +1,41 @@ +import type { KiloClient } from "@kilocode/sdk/v2/client" + +export interface ForkHandoffInput { + client: KiloClient + sessionId: string + directory?: string +} + +export function forkText(input: Pick): string { + return [ + "", + "This session was forked from an existing session in the current repository or worktree.", + ...(input.directory + ? [ + `Use this as the current working directory: ${input.directory}`, + "For this fork, this location supersedes any earlier repository or worktree location retained in the copied context.", + ] + : []), + "The prior conversation context was retained intentionally.", + "The user may continue the same task, explore an alternative approach, or provide new instructions.", + "Follow the user's next instruction as the direction for this fork, using retained context when relevant.", + "", + ].join("\n") +} + +export async function recordForkHandoff(input: ForkHandoffInput): Promise { + const payload = { + sessionID: input.sessionId, + ...(input.directory ? { directory: input.directory } : {}), + noReply: true, + parts: [ + { + type: "text", + text: forkText(input), + synthetic: true, + }, + ], + } as Parameters[0] + + await input.client.session.promptAsync(payload, { throwOnError: true }) +} diff --git a/packages/kilo-vscode/src/agent-manager/fork-session.ts b/packages/kilo-vscode/src/agent-manager/fork-session.ts index c78e8086b14..d6d4118c6aa 100644 --- a/packages/kilo-vscode/src/agent-manager/fork-session.ts +++ b/packages/kilo-vscode/src/agent-manager/fork-session.ts @@ -3,10 +3,12 @@ import { getErrorMessage } from "../kilo-provider-utils" import { TelemetryProxy, TelemetryEventName } from "../services/telemetry" import type { WorktreeStateManager } from "./WorktreeStateManager" import { PLATFORM } from "./constants" +import { recordForkHandoff } from "./fork-handoff" export interface ForkContext { getClient: () => KiloClient state: WorktreeStateManager | undefined + directory: string | undefined postError: (message: string) => void registerWorktreeSession: (sessionId: string, directory: string) => void pushState: () => void @@ -37,8 +39,8 @@ export async function forkSession( } const directory = (() => { - if (!worktreeId || !ctx.state) return undefined - return ctx.state.getWorktree(worktreeId)?.path + if (!worktreeId || !ctx.state) return ctx.directory + return ctx.state.getWorktree(worktreeId)?.path ?? ctx.directory })() let forked: Session @@ -63,6 +65,10 @@ export async function forkSession( if (directory) ctx.registerWorktreeSession(forked.id, directory) } + await recordForkHandoff({ client, sessionId: forked.id, directory }).catch((err) => { + ctx.log("forkSession: failed to record fork handoff:", getErrorMessage(err)) + }) + ctx.pushState() ctx.notifyForked(forked, sessionId, worktreeId) ctx.registerSession(forked) diff --git a/packages/kilo-vscode/src/features.ts b/packages/kilo-vscode/src/features.ts index 889c12d8e71..0e0423b9046 100644 --- a/packages/kilo-vscode/src/features.ts +++ b/packages/kilo-vscode/src/features.ts @@ -4,7 +4,6 @@ type PluginSpec = string | [string, Record] type ConfigLike = { plugin?: readonly PluginSpec[] | null - experimental?: { semantic_indexing?: boolean } | null } export type Features = { @@ -13,6 +12,6 @@ export type Features = { export function configFeatures(config?: ConfigLike | null): Features { return { - indexing: hasIndexingPlugin(config?.plugin ?? []) && config?.experimental?.semantic_indexing === true, + indexing: hasIndexingPlugin(config?.plugin ?? []), } } diff --git a/packages/kilo-vscode/src/kilo-provider/fork-session.ts b/packages/kilo-vscode/src/kilo-provider/fork-session.ts index 1416320581c..d97fc213a0b 100644 --- a/packages/kilo-vscode/src/kilo-provider/fork-session.ts +++ b/packages/kilo-vscode/src/kilo-provider/fork-session.ts @@ -32,6 +32,7 @@ export async function handleForkSession(ctx: ForkContext, sessionId: string, mes { getClient: () => ctx.connection.getClient(), state: undefined, + directory: ctx.directory(sessionId), postError: (message) => ctx.post({ type: "error", message }), registerWorktreeSession: () => {}, pushState: () => {}, diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts new file mode 100644 index 00000000000..4600a9f0cd6 --- /dev/null +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -0,0 +1,64 @@ +import AxeBuilder from "@axe-core/playwright" +import { expect, test, type Locator, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" +const RULES = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22a", "wcag22aa"] + +// Explicitly ratchet in repaired/stable workflows rather than making existing +// untriaged Storybook findings block unrelated webview changes. +const STORIES = [ + { id: "profile--not-logged-in", name: "Profile / not logged in" }, + { id: "profile--logged-in-personal", name: "Profile / personal account" }, + { id: "profile--logged-in", name: "Profile / organization account" }, + { id: "settings--providers-configure", name: "Settings / providers empty state" }, + { id: "marketplace--skills-tab-empty", name: "Marketplace / skills empty state" }, + { id: "marketplace--agents-tab-empty", name: "Marketplace / agents empty state" }, +] + +function url(id: string) { + return `/iframe.html?id=${id}&viewMode=story&globals=${GLOBALS}` +} + +async function open(page: Page, id: string) { + await page.goto(url(id), { waitUntil: "load" }) + await page.waitForSelector("#storybook-root *", { state: "attached" }) +} + +async function scan(page: Page) { + const result = await new AxeBuilder({ page }).include("#storybook-root").withTags(RULES).analyze() + const details = result.violations + .map((item) => `${item.id}: ${item.help}\n${item.nodes.map((node) => ` ${node.target.join(" ")}`).join("\n")}`) + .join("\n") + + expect(result.violations, details).toEqual([]) +} + +async function reach(page: Page, target: Locator) { + for (let step = 0; step < 10; step++) { + await page.keyboard.press("Tab") + if (await target.evaluate((node) => node === document.activeElement)) return + } +} + +test.describe("webview accessibility ratchet", () => { + for (const story of STORIES) { + test(`${story.name} passes automated WCAG checks`, async ({ page }) => { + await open(page, story.id) + await scan(page) + }) + } + + test("Profile login exposes a keyboard-operable named control", async ({ page }) => { + await open(page, "profile--not-logged-in") + + const login = page.getByRole("button", { name: "Login with Kilo Code" }) + await reach(page, login) + await expect(login).toBeFocused() + + await login.evaluate((node) => { + node.addEventListener("click", () => node.setAttribute("data-keyboard-activated", "true"), { once: true }) + }) + await page.keyboard.press("Enter") + await expect(login).toHaveAttribute("data-keyboard-activated", "true") + }) +}) diff --git a/packages/kilo-vscode/tests/history-accessibility.spec.ts b/packages/kilo-vscode/tests/history-accessibility.spec.ts new file mode 100644 index 00000000000..554dd3ee00d --- /dev/null +++ b/packages/kilo-vscode/tests/history-accessibility.spec.ts @@ -0,0 +1,102 @@ +import { expect, test, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" + +function story(page: Page, id: string) { + return page.goto(`/iframe.html?id=${id}&viewMode=story&globals=${GLOBALS}`, { waitUntil: "load" }) +} + +test.describe("history session accessibility", () => { + test("opens a selected session through a standalone named row control", async ({ page }) => { + await story(page, "history-sessionlist--with-items") + + const row = page.getByRole("button", { name: /Refactor authentication module.*Current session/ }) + await expect(row).toHaveAttribute("data-selected", "true") + await expect(page.locator('[data-slot="list-item"] button')).toHaveCount(0) + + await row.focus() + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="selected-session"]')).toHaveText("s1") + }) + + test("announces the active filtered result before Enter opens it", async ({ page }) => { + await story(page, "history-sessionlist--with-items") + + const search = page.getByPlaceholder("Search sessions...") + await search.fill("screenshot") + await expect(search).toBeFocused() + await expect(page.locator('[data-slot="session-list-status"]')).toHaveText("Add screenshot test coverage") + + await page.keyboard.press("Enter") + await expect(page.locator('[data-slot="selected-session"]')).toHaveText("s2") + }) + + test("focuses row actions without opening a session during rename", async ({ page }) => { + await story(page, "history-sessionlist--with-items") + + const selected = page.locator('[data-slot="selected-session"]') + const rename = page.getByRole("button", { name: "Rename: Add screenshot test coverage" }) + await rename.focus() + await expect(rename).toBeFocused() + await page.keyboard.press("Enter") + const input = page.getByRole("textbox", { name: "Rename" }) + await expect(input).toBeFocused() + await expect(selected).toBeEmpty() + + await input.fill("Updated screenshot test coverage") + await page.keyboard.press("Enter") + await expect(input).toBeHidden() + await expect(selected).toBeEmpty() + + const renamed = page.getByRole("button", { name: "Rename: Add screenshot test coverage" }) + await renamed.focus() + await page.keyboard.press("Enter") + await expect(input).toBeFocused() + await page.keyboard.press("Escape") + await expect(input).toBeHidden() + await expect(selected).toBeEmpty() + }) + + test("deleting does not open a session and restores focus after cancel", async ({ page }) => { + await story(page, "history-sessionlist--with-items") + + const selected = page.locator('[data-slot="selected-session"]') + const remove = page.getByRole("button", { name: "Delete session: Add screenshot test coverage" }) + await remove.focus() + await expect(remove).toBeFocused() + await page.keyboard.press("Enter") + await expect(page.getByRole("dialog", { name: "Delete session" })).toBeVisible() + await expect(selected).toBeEmpty() + + await page.getByRole("button", { name: "Cancel" }).click() + await expect(page.getByRole("dialog", { name: "Delete session" })).toBeHidden() + await expect(remove).toBeFocused() + await expect(selected).toBeEmpty() + }) + + test("exposes Local and Cloud as keyboard navigable selected tabs", async ({ page }) => { + await story(page, "history-sessionlist--sources") + + const local = page.getByRole("tab", { name: "Local" }) + const cloud = page.getByRole("tab", { name: "Cloud" }) + await expect(page.getByRole("tablist", { name: "History source" })).toBeVisible() + await expect(local).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Local" })).toBeVisible() + + await local.focus() + await page.keyboard.press("ArrowRight") + await expect(cloud).toBeFocused() + await expect(local).toHaveAttribute("aria-selected", "true") + await page.keyboard.press("Enter") + await expect(cloud).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Cloud" })).toBeVisible() + await expect(page.getByPlaceholder("Search sessions...")).toBeFocused() + + await cloud.focus() + await page.keyboard.press("ArrowLeft") + await expect(local).toBeFocused() + await page.keyboard.press("Enter") + await expect(local).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Local" })).toBeVisible() + }) +}) diff --git a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts new file mode 100644 index 00000000000..dd634e67ff9 --- /dev/null +++ b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts @@ -0,0 +1,151 @@ +import { expect, test, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" + +function story(id: string) { + return `/iframe.html?id=${id}&viewMode=story&globals=${GLOBALS}` +} + +async function load(page: Page, id: string) { + await page.goto(story(id), { waitUntil: "load" }) + await page.waitForSelector("#storybook-root *", { state: "attached" }) +} + +test("model selector exposes combobox relationships and active option movement", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + const listbox = page.getByRole("listbox", { name: "Review model" }) + const alpha = page.getByRole("option", { name: "Alpha" }) + const bravo = page.getByRole("option", { name: "Bravo" }) + + await expect(combobox).toBeFocused() + await expect(combobox).toHaveAttribute("aria-expanded", "true") + await expect(combobox).toHaveAttribute("aria-controls", await listbox.getAttribute("id")) + await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id")) + await expect(combobox).toHaveAccessibleDescription("Choose the model used for code review tasks.") + await expect(alpha.locator("button")).toHaveCount(0) + await expect(page.getByRole("button", { name: "Add to favorites: Alpha" })).toBeVisible() + + await combobox.press("ArrowDown") + await expect(combobox).toBeFocused() + await expect(combobox).toHaveAttribute("aria-activedescendant", await bravo.getAttribute("id")) + + const expand = page.getByRole("button", { name: "Expand" }) + const controls = await expand.getAttribute("aria-controls") + const preview = page.locator(`[id="${controls}"]`) + await expect(expand).toHaveAttribute("aria-expanded", "false") + await expect(preview).toHaveAttribute("aria-hidden", "true") + await expect(preview.locator("button, a, [tabindex]")).toHaveCount(0) + await expand.click() + const collapse = page.getByRole("button", { name: "Collapse", exact: true }) + await expect(collapse).toHaveAttribute("aria-controls", controls!) + await expect(collapse).toHaveAttribute("aria-expanded", "true") + await expect(preview).toHaveAttribute("aria-hidden", "false") + await expect(preview.getByRole("button", { name: "Add to favorites" })).toBeVisible() +}) + +test("expanded preview waits for explicit pointer selection", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + await page.getByRole("button", { name: "Expand" }).click() + await page.getByRole("option", { name: "Bravo" }).click() + + await expect(page.getByTestId("model-selector-value")).toHaveText("alpha") + await expect(page.getByRole("combobox", { name: "Review model: Alpha. Search models" })).toBeVisible() + await expect(page.locator(".model-selector-preview")).toContainText("Bravo") + + await page.getByRole("button", { name: "Select: Bravo" }).click() + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") +}) + +test("selected favorite remains selected when its duplicate group is collapsed", async ({ page }) => { + await load(page, "shared--model-selector-selected-favorite") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + const alpha = page.getByRole("option", { name: "Alpha" }) + const favorites = page.getByRole("button", { name: "Collapse Favorites" }) + await expect(alpha.first()).toHaveAttribute("aria-selected", "true") + await expect.poll(() => favorites.evaluate((el) => getComputedStyle(el).borderTopStyle)).toBe("solid") + + await favorites.click() + await expect(alpha).toHaveCount(1) + await expect(alpha).toHaveAttribute("aria-selected", "true") + await expect(combobox).toHaveAttribute("aria-activedescendant", await alpha.getAttribute("id")) +}) + +test("Enter selects the active option and Escape restores selector focus", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + await combobox.press("ArrowDown") + await combobox.press("Enter") + + const trigger = page.getByRole("button", { name: "Review model: Bravo" }) + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") + await expect(trigger).toBeFocused() + + await trigger.click() + const reopened = page.getByRole("combobox", { name: "Review model: Bravo. Search models" }) + await reopened.press("ArrowDown") + await reopened.press("Escape") + + await expect(page.getByTestId("model-selector-value")).toHaveText("bravo") + await expect(trigger).toBeFocused() +}) + +test("no-match search announces the empty result and can choose the default option", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + const combobox = page.getByRole("combobox", { name: "Review model: Alpha. Search models" }) + await combobox.fill("no matching model") + + await expect(page.locator(".model-selector-empty")).toHaveText("No model results") + const clear = page.getByRole("option", { name: "Use default model" }) + await expect(combobox).toHaveAttribute("aria-activedescendant", await clear.getAttribute("id")) + await combobox.press("Enter") + + await expect(page.getByTestId("model-selector-value")).toHaveText("default") + await expect(page.getByRole("button", { name: "Review model: Use default model" })).toBeFocused() +}) + +test("settings and mode editing expose distinct model field purposes", async ({ page }) => { + await load(page, "settings--models-accessible-labels") + + await expect(page.getByRole("button", { name: "Default Model: Not set" })).toHaveAccessibleDescription( + "Primary model for conversations", + ) + await expect(page.getByRole("button", { name: "Small Model: Not set" })).toHaveAccessibleDescription( + /Lightweight model/, + ) + await expect(page.getByRole("button", { name: "Subagent Model: Not set" })).toHaveAccessibleDescription( + /Default model and reasoning effort/, + ) + await expect(page.getByRole("button", { name: "Autocomplete model: Not set" })).toHaveAccessibleDescription( + "Select the model used for inline code completions", + ) + await expect(page.getByRole("button", { name: "Model per Mode: code: Not set" })).toHaveAccessibleDescription( + /Override the default model for specific modes/, + ) + + await load(page, "settings--mode-edit-export") + await expect(page.getByRole("button", { name: /Model Override:/ })).toHaveAccessibleDescription( + "Override the default model for this agent", + ) +}) + +test("chat picker Escape returns focus to the prompt", async ({ page }) => { + await load(page, "prompt-input--default-420") + + await page.getByRole("button", { name: /^Select model:/ }).click() + const combobox = page.getByRole("combobox", { name: /^Select model:.*Search models$/ }) + await expect(combobox).toBeFocused() + await combobox.press("Escape") + + await expect(page.locator("textarea.prompt-input")).toBeFocused() +}) diff --git a/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts b/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts index c2dab4a182a..4b72acdcac0 100644 --- a/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts +++ b/packages/kilo-vscode/tests/unit/continue-in-worktree.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "bun:test" +import { describe, expect, it, mock } from "bun:test" import { abortSession, captureState, @@ -8,6 +8,7 @@ import { type StepResult, } from "../../src/agent-manager/continue-in-worktree" import type { CreateWorktreeResult } from "../../src/agent-manager/WorktreeManager" +import { forkText } from "../../src/agent-manager/fork-handoff" import type { Session } from "@kilocode/sdk/v2/client" const noop = () => {} @@ -100,17 +101,27 @@ describe("continue-in-worktree steps", () => { if (!res.ok) expect(res.error).toContain("fork failed") }) - it("returns forked session on success", async () => { + it("records handoff instructions for the forked worktree session", async () => { const forked = session("forked-1") + const promptAsync = mock(async () => ({})) const c = ctx({ getClient: () => ({ - session: { fork: () => Promise.resolve({ data: forked }) }, + session: { fork: () => Promise.resolve({ data: forked }), promptAsync }, }) as never, }) const res = await forkSession(c, "session-1", "/tmp/wt") expect(res.ok).toBe(true) if (res.ok) expect(res.value.id).toBe("forked-1") + expect(promptAsync).toHaveBeenCalledWith( + { + sessionID: "forked-1", + directory: "/tmp/wt", + noReply: true, + parts: [{ type: "text", text: forkText({ directory: "/tmp/wt" }), synthetic: true }], + }, + { throwOnError: true }, + ) }) }) diff --git a/packages/kilo-vscode/tests/unit/fork-handoff.test.ts b/packages/kilo-vscode/tests/unit/fork-handoff.test.ts new file mode 100644 index 00000000000..9fb775cd220 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/fork-handoff.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, mock } from "bun:test" +import { forkText, recordForkHandoff } from "../../src/agent-manager/fork-handoff" + +describe("fork handoff", () => { + it("describes retained context without assuming a new task", () => { + const text = forkText({ directory: "/repo/.kilo/worktrees/feature" }) + + expect(text).toContain("This session was forked from an existing session in the current repository or worktree.") + expect(text).toContain("Use this as the current working directory: /repo/.kilo/worktrees/feature") + expect(text).toContain("this location supersedes any earlier repository or worktree location") + expect(text).toContain("The prior conversation context was retained intentionally.") + expect(text).toContain("continue the same task, explore an alternative approach, or provide new instructions") + expect(text).toContain("Follow the user's next instruction as the direction for this fork") + }) + + it("records a hidden no-reply handoff in the forked session", async () => { + const promptAsync = mock(async () => ({})) + const client = { session: { promptAsync } } + + await recordForkHandoff({ + client: client as never, + sessionId: "session-fork", + directory: "/repo/.kilo/worktrees/feature", + }) + + expect(promptAsync).toHaveBeenCalledWith( + { + sessionID: "session-fork", + directory: "/repo/.kilo/worktrees/feature", + noReply: true, + parts: [ + { + type: "text", + text: forkText({ directory: "/repo/.kilo/worktrees/feature" }), + synthetic: true, + }, + ], + }, + { throwOnError: true }, + ) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/fork-session.test.ts b/packages/kilo-vscode/tests/unit/fork-session.test.ts new file mode 100644 index 00000000000..b5526c2688c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/fork-session.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, mock } from "bun:test" +import type { Session } from "@kilocode/sdk/v2/client" +import { forkText } from "../../src/agent-manager/fork-handoff" +import { forkSession, type ForkContext } from "../../src/agent-manager/fork-session" + +const noop = () => {} + +function session(id: string): Session { + return { id, title: id, createdAt: "", updatedAt: "" } as Session +} + +function ctx(client: unknown, overrides: Partial = {}): ForkContext { + return { + getClient: () => client as never, + state: undefined, + directory: "/repo", + postError: noop, + registerWorktreeSession: noop, + pushState: noop, + notifyForked: noop, + registerSession: noop, + log: noop, + ...overrides, + } +} + +describe("agent manager fork session", () => { + it("records the hidden handoff in the current repository", async () => { + const fork = mock(async () => ({ data: session("forked") })) + const promptAsync = mock(async () => ({})) + const client = { session: { fork, promptAsync } } + + await forkSession(ctx(client), "source", undefined, "message") + + expect(fork).toHaveBeenCalledWith( + { sessionID: "source", directory: "/repo", messageID: "message" }, + { throwOnError: true }, + ) + expect(promptAsync).toHaveBeenCalledWith( + { + sessionID: "forked", + directory: "/repo", + noReply: true, + parts: [{ type: "text", text: forkText({ directory: "/repo" }), synthetic: true }], + }, + { throwOnError: true }, + ) + }) + + it("uses the selected worktree directory for the handoff", async () => { + const fork = mock(async () => ({ data: session("forked") })) + const promptAsync = mock(async () => ({})) + const client = { session: { fork, promptAsync } } + const state = { + getWorktree: () => ({ path: "/repo/.kilo/worktrees/feature" }), + addSession: mock(() => undefined), + } + + await forkSession(ctx(client, { state: state as never }), "source", "worktree") + + expect(fork).toHaveBeenCalledWith( + { sessionID: "source", directory: "/repo/.kilo/worktrees/feature" }, + { throwOnError: true }, + ) + expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ directory: "/repo/.kilo/worktrees/feature" }), { + throwOnError: true, + }) + }) + + it("still exposes the fork when recording the handoff fails", async () => { + const notify = mock(() => undefined) + const log = mock(() => undefined) + const client = { + session: { + fork: mock(async () => ({ data: session("forked") })), + promptAsync: mock(async () => { + throw new Error("handoff failed") + }), + }, + } + + await forkSession(ctx(client, { notifyForked: notify, log }), "source") + + expect(notify).toHaveBeenCalledWith(expect.objectContaining({ id: "forked" }), "source", undefined) + expect(log).toHaveBeenCalledWith("forkSession: failed to record fork handoff:", "handoff failed") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts index cd91df1ecff..0d20e40d656 100644 --- a/packages/kilo-vscode/tests/unit/indexing-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/indexing-utils.test.ts @@ -85,37 +85,19 @@ describe("indexing SSE mapping", () => { }) describe("indexing feature detection", () => { - it("requires experimental.semantic_indexing when indexing plugin is present", () => { - expect(configFeatures({ plugin: ["kilo-indexing"] }).indexing).toBe(false) - expect(configFeatures({ plugin: ["kilo-indexing"], experimental: {} }).indexing).toBe(false) - expect(configFeatures({ plugin: ["kilo-indexing"], experimental: { semantic_indexing: false } }).indexing).toBe( - false, - ) + it("enables indexing settings when the indexing plugin is present", () => { + expect(configFeatures({ plugin: ["kilo-indexing"] }).indexing).toBe(true) }) - it("detects supported indexing plugin specifiers when experimental.semantic_indexing is true", () => { - expect(configFeatures({ plugin: ["kilo-indexing"], experimental: { semantic_indexing: true } }).indexing).toBe(true) - expect( - configFeatures({ plugin: ["kilo-indexing@1.2.3"], experimental: { semantic_indexing: true } }).indexing, - ).toBe(true) - expect( - configFeatures({ plugin: ["@kilocode/kilo-indexing"], experimental: { semantic_indexing: true } }).indexing, - ).toBe(true) - expect( - configFeatures({ plugin: ["@kilocode/kilo-indexing@1.2.3"], experimental: { semantic_indexing: true } }).indexing, - ).toBe(true) - expect( - configFeatures({ - plugin: ["file:///tmp/.opencode/plugin/kilo-indexing.js"], - experimental: { semantic_indexing: true }, - }).indexing, - ).toBe(true) - expect( - configFeatures({ - plugin: ["file:///tmp/node_modules/@kilocode/kilo-indexing/index.js"], - experimental: { semantic_indexing: true }, - }).indexing, - ).toBe(true) + it("detects supported indexing plugin specifiers", () => { + expect(configFeatures({ plugin: ["kilo-indexing"] }).indexing).toBe(true) + expect(configFeatures({ plugin: ["kilo-indexing@1.2.3"] }).indexing).toBe(true) + expect(configFeatures({ plugin: ["@kilocode/kilo-indexing"] }).indexing).toBe(true) + expect(configFeatures({ plugin: ["@kilocode/kilo-indexing@1.2.3"] }).indexing).toBe(true) + expect(configFeatures({ plugin: ["file:///tmp/.opencode/plugin/kilo-indexing.js"] }).indexing).toBe(true) + expect(configFeatures({ plugin: ["file:///tmp/node_modules/@kilocode/kilo-indexing/index.js"] }).indexing).toBe( + true, + ) }) it("ignores unrelated plugin lists", () => { diff --git a/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx b/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx index 5314ca57033..79e7d8b8ff5 100644 --- a/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/history/CloudSessionList.tsx @@ -63,9 +63,11 @@ const CloudSessionList: Component = (props) => { const [gitUrl, setGitUrl] = createSignal(null) const [repoOnly, setRepoOnly] = createSignal(true) const [initialized, setInitialized] = createSignal(false) + const [notice, setNotice] = createSignal("") let loadGen = 0 let activeGen = 0 + let seq = 0 const unsub = vscode.onMessage((message: ExtensionMessage) => { if (message.type === "cloudSessionsLoaded") { @@ -110,6 +112,16 @@ const CloudSessionList: Component = (props) => { }) }) + function announce(s: DisplaySession | undefined) { + const id = ++seq + setNotice("") + if (!s) return + queueMicrotask(() => { + if (id !== seq) return + setNotice(s.title) + }) + } + function loadMore() { const cursor = nextCursor() if (!cursor || loading()) return @@ -130,6 +142,7 @@ const CloudSessionList: Component = (props) => { items={sessions()} key={(s) => s.id} filterKeys={["title"]} + onMove={announce} onSelect={(s) => { if (s) props.onSelectSession?.(s.id) }} @@ -161,6 +174,9 @@ const CloudSessionList: Component = (props) => { )} +
+ {notice()} +
-
+
@@ -81,12 +116,25 @@ const HistoryView: Component = (props) => {
-
- {tab() === "local" ? ( - - ) : ( - - )} + +
) diff --git a/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx b/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx index 6a0cbed69d4..6d412adb77d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx @@ -47,6 +47,8 @@ const SessionList: Component = (props) => { const [renamingId, setRenamingId] = createSignal(null) const [pendingRenameId, setPendingRenameId] = createSignal(null) + const [notice, setNotice] = createSignal("") + let seq = 0 onMount(() => { console.log("[Kilo New] SessionList mounted, loading sessions") @@ -74,36 +76,89 @@ const SessionList: Component = (props) => { setRenamingId(null) } - function confirmDelete(s: SessionInfo) { - dialog.show(() => ( - -
- {language.t("session.delete.confirm", { name: s.title || language.t("session.untitled") })} -
- - + function name(s: SessionInfo) { + return s.title || language.t("session.untitled") + } + + function label(action: string, s: SessionInfo) { + return `${action}: ${name(s)}` + } + + function announce(s: SessionInfo | undefined) { + const id = ++seq + setNotice("") + if (!s) return + queueMicrotask(() => { + if (id !== seq) return + const current = session.currentSessionID() === s.id ? `. ${language.t("session.current")}` : "" + setNotice(`${name(s)}${current}`) + }) + } + + function confirmDelete(s: SessionInfo, restore?: HTMLElement) { + dialog.show( + () => ( + +
+ {language.t("session.delete.confirm", { name: name(s) })} +
+ + +
-
-
- )) + + ), + () => { + queueMicrotask(() => { + if (restore?.isConnected) restore.focus() + }) + }, + ) } function wrapItem(item: SessionInfo, node: JSX.Element): JSX.Element { return ( - - {node} + + + {node} + startRename(item)} + /> + confirmDelete(item, event.currentTarget)} + /> + + } + > +
+ +
+
= (props) => { key={(s) => s.id} filterKeys={["title"]} current={currentSession()} + onMove={announce} onSelect={(s) => { if (s && renamingId() !== s.id) { props.onSelectSession(s.id) @@ -153,42 +209,18 @@ const SessionList: Component = (props) => { itemWrapper={wrapItem} > {(s) => ( - - {s.title || language.t("session.untitled")} - {formatRelativeDate(s.updatedAt)} - { - e.stopPropagation() - startRename(s) - }} - > - - - { - e.stopPropagation() - confirmDelete(s) - }} - > - - - - } - > - - + <> + {name(s)} + {formatRelativeDate(s.updatedAt)} + + {language.t("session.current")} + + )} +
+ {notice()} +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 320153907f1..77d65f6779f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -164,19 +164,6 @@ const ExperimentalTab: Component = () => { - - updateExperimental("semantic_indexing", checked)} - hideLabel - > - {language.t("settings.experimental.semanticIndexing.title")} - - - = (props) => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.agentBehaviour.modelOverride.title")} + description={language.t("settings.agentBehaviour.modelOverride.description")} /> diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index c01e0434094..a3fc83d1c21 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -101,6 +101,8 @@ const ModelsTab: Component = () => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.providers.defaultModel.title")} + description={language.t("settings.providers.defaultModel.description")} /> { allowClear clearLabel={language.t("settings.providers.notSet")} includeAutoSmall + label={language.t("settings.providers.smallModel.title")} + description={language.t("settings.providers.smallModel.description")} /> { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.providers.subagentModel.title")} + description={language.t("settings.providers.subagentModel.description")} /> { favorites={false} allowClear clearLabel={language.t("settings.providers.notSet")} + label={language.t("settings.autocomplete.model.title")} + description={language.t("settings.autocomplete.model.description")} /> @@ -167,6 +175,8 @@ const ModelsTab: Component = () => { placement="bottom-start" allowClear clearLabel={language.t("settings.providers.notSet")} + label={`${language.t("settings.providers.modeModels")}: ${agent.name}`} + description={language.t("settings.providers.modeModels.description")} /> )} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx index 6dbfd0174ef..f5e4f71ea3b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ProviderConnectDialog.tsx @@ -380,7 +380,21 @@ const ProviderConnectDialog: Component = (props) => {state.error}
-
+
+ diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index 98698a5a6d7..4ae79db99d6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -7,7 +7,18 @@ * ModelSelector — thin wrapper wired to session context for chat usage. */ -import { createSignal, createMemo, createEffect, onCleanup, For, Show, createSelector, useContext } from "solid-js" +import { + createSignal, + createMemo, + createEffect, + createUniqueId, + onCleanup, + For, + Show, + createSelector, + useContext, + untrack, +} from "solid-js" import type { Accessor, Component } from "solid-js" import { PopupSelector } from "./PopupSelector" import { Button } from "@kilocode/kilo-ui/button" @@ -98,6 +109,10 @@ export interface ModelSelectorBaseProps { deferDismiss?: boolean /** Render inline instead of through a portal when nested in a dialog. */ portal?: boolean + /** Accessible purpose of this model setting or selector. */ + label?: string + /** Additional accessible context for this model setting. */ + description?: string } export const ModelSelectorBase: Component = (props) => { @@ -106,6 +121,12 @@ export const ModelSelectorBase: Component = (props) => { // Session context is optional — ModelSelectorBase is also used in Settings // where SessionProvider may not be mounted. const session = useContext(SessionContext) + const uid = createUniqueId() + const listID = `${uid}-models` + const previewID = `${uid}-preview` + const descriptionID = `${uid}-description` + const optionID = (key: string) => `${uid}-option-${encodeURIComponent(key)}` + const groupID = (key: string) => `${uid}-group-${encodeURIComponent(key)}` const activeModel = () => { const items = props.models if (items) return items.find((m) => m.providerID === props.value?.providerID && m.id === props.value?.modelID) @@ -312,9 +333,16 @@ export const ModelSelectorBase: Component = (props) => { const activeKey = (m?: EnrichedModel | null) => { if (!m) return props.allowClear ? CLEAR_KEY : defaultKey() const key = modelKey(m.providerID, m.id) - if (!debouncedSearch() && favoriteKeys().has(key)) return favoriteKey(m) + const favorite = favoriteKey(m) + if (!debouncedSearch() && favoriteKeys().has(key) && rowMap().has(favorite)) return favorite return canonicalKey(m) } + const chosen = (row: ModelRow) => { + if (row.kind === "clear") return !props.value?.providerID + if (!row.model || !isActive(row.model)) return false + return activeKey(row.model) === row.key + } + const activeOptionID = () => (rowMap().has(selectedKey()) ? optionID(selectedKey()) : undefined) const [anchor, setAnchor] = createSignal(null) const previewModel = createMemo(() => rowMap().get(previewKey() ?? "")?.model ?? null) @@ -329,7 +357,11 @@ export const ModelSelectorBase: Component = (props) => { // which resets selection. createEffect(() => { rows() // track - setSelectedKey((prev) => (rowMap().has(prev) ? prev : defaultKey())) + setSelectedKey((prev) => { + if (rowMap().has(prev)) return prev + const next = untrack(() => activeKey(activeModel())) + return rowMap().has(next) ? next : defaultKey() + }) setPreActiveKey((prev) => (prev && rowMap().has(prev) ? prev : null)) setPreviewKey((prev) => (prev && rowMap().has(prev) ? prev : null)) }) @@ -395,7 +427,10 @@ export const ModelSelectorBase: Component = (props) => { // Listen for slash command trigger const onTrigger = () => setOpen(true) window.addEventListener("openModelPicker", onTrigger) - onCleanup(() => window.removeEventListener("openModelPicker", onTrigger)) + onCleanup(() => { + window.removeEventListener("openModelPicker", onTrigger) + clearTimeout(previewTimer) + }) const onEscape = (e: KeyboardEvent) => { if (!open() || e.key !== "Escape") return @@ -545,247 +580,301 @@ export const ModelSelectorBase: Component = (props) => { notSet: language.t("dialog.model.notSet"), }, ) + const label = () => props.label ?? language.t("dialog.model.select.title") + const controlLabel = () => `${label()}: ${triggerLabel()}` + const searchLabel = () => `${controlLabel()}. ${language.t("dialog.model.search.placeholder")}` + const describedBy = () => (props.description ? descriptionID : undefined) return ( - - {triggerLabel()} - - - - - } - class={`model-selector-popover${expanded() ? " model-selector-popover--expanded" : ""}`} - > - {(bodyH) => { - createEffect(() => { - if (!expanded()) return - const h = bodyH() - if (h === undefined) return - const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0) - setPreviewHeight((h - chrome) / 2) - }) - return ( -
-
- setSearch(e.currentTarget.value)} - /> - - { - setExpanded((v) => { - if (v) { - setPreActiveKey(null) - setPreviewKey(null) - } - return !v - }) - requestAnimationFrame(() => { - searchRef?.focus() - scrollRow(preActiveKey() ?? selectedKey(), "nearest") - }) - }} + <> + + + {props.description} + + + + {triggerLabel()} + + + + + } + class={`model-selector-popover${expanded() ? " model-selector-popover--expanded" : ""}`} + > + {(bodyH) => { + createEffect(() => { + if (!expanded()) return + const h = bodyH() + if (h === undefined) return + const chrome = (searchWrapperRef?.offsetHeight ?? 0) + (splitterRef?.offsetHeight ?? 0) + setPreviewHeight((h - chrome) / 2) + }) + return ( +
+
+ setSearch(e.currentTarget.value)} /> - -
- -
- -
{language.t("dialog.model.empty")}
-
- - -
pickClear()} - onMouseMove={() => { - setPointer(true) - }} - onMouseEnter={() => { - if (pointer()) setSelectedKey(CLEAR_KEY) - }} + - - {props.clearLabel ?? language.t("dialog.model.notSet")} - -
-
+ { + setExpanded((v) => { + if (v) { + setPreActiveKey(null) + setPreviewKey(null) + } + return !v + }) + requestAnimationFrame(() => { + searchRef?.focus() + scrollRow(preActiveKey() ?? selectedKey(), "nearest") + }) + }} + /> + +
- - {(group) => { - const shown = () => isGroupOpen(group.key) - return ( - <> - - - - {(row) => { - if (!row.model) return null - const model = row.model - const hovered = () => isSelected(row.key) - const preActive = () => isPreActive(row.key) - const showSelectBtn = () => expanded() && preActive() && !isActive(model) - const starred = () => favoriteKeys().has(modelKey(model.providerID, model.id)) - const showProvider = () => row.kind === "favorite" - return ( -
{ - refs.set(row.key, el) - onCleanup(() => refs.delete(row.key)) - }} - class={`model-selector-item${(hovered() && !pointer()) || preActive() ? " keyboard-focused" : ""}${hovered() || preActive() ? " selected" : ""}${isActive(model) && row.kind === "model" ? " active" : ""}`} - role="option" - aria-selected={isActive(model) && row.kind === "model"} - onClick={() => { - setRow(row.key) - setPreviewKey(row.key) - if (!expanded()) selectRow(row) - searchRef?.focus() - }} - onDblClick={() => { - if (expanded()) selectRow(row) - }} - onMouseMove={() => { - setPointer(true) - }} - onMouseEnter={() => { - if (pointer()) setSelectedKey(row.key) - }} - > -
- - {(() => { - const full = sanitizeName(model.name) - const sep = full.indexOf(": ") - if (sep < 0) return {full} - return ( - <> - {full.slice(0, sep)} - {full.slice(sep + 2)} - - ) - })()} - - - {language.t("model.tag.free")} + + {group.label} + + + + + + {(row) => { + if (!row.model) return null + const model = row.model + const hovered = () => isSelected(row.key) + const preActive = () => isPreActive(row.key) + const starred = () => favoriteKeys().has(modelKey(model.providerID, model.id)) + const showProvider = () => row.kind === "favorite" + const showSelect = () => expanded() && preActive() && !isActive(model) + const starLabel = () => + `${starred() ? language.t("model.favorite.remove") : language.t("model.favorite.add")}: ${sanitizeName(model.name)}` + return ( + - - - - - - -
- ) - }} - - - - ) - }} - -
+ ) + }} +
+
+
+ ) + }} + +
- -
- -
- + +
+ +
+ + + +
-
- ) - }} - + ) + }} + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 8afe59af40a..585fd2db795 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -973,6 +973,8 @@ export const dict = { "session.delete.confirm": 'حذف الجلسة "{{name}}"؟', "session.delete.button": "حذف الجلسة", "session.untitled": "بدون عنوان", + "session.current": "الجلسة الحالية", + "session.history.sources": "مصدر السجل", "session.recent": "الأخيرة", "session.showHistory": "عرض السجل", "session.search.placeholder": "البحث في الجلسات...", @@ -1234,9 +1236,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "عدم تلخيص المحتوى الملصق الكبير", "settings.experimental.batch.title": "أداة دفعية", "settings.experimental.batch.description": "تمكين المعالجة الدفعية لاستدعاءات الأدوات", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "بحث في قاعدة الكود", "settings.experimental.codebaseSearch.description": "تمكين البحث بالذكاء الاصطناعي باللغة الطبيعية عبر قاعدة الكود", "settings.experimental.speechToText.title": "تحويل الصوت إلى نص", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 4ed3ec97abe..d3b5a22f098 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -988,6 +988,8 @@ export const dict = { "session.delete.confirm": 'Excluir sessão "{{name}}"?', "session.delete.button": "Excluir sessão", "session.untitled": "Sem título", + "session.current": "Sessão atual", + "session.history.sources": "Fonte do histórico", "session.recent": "Recentes", "session.showHistory": "Mostrar Histórico", "session.search.placeholder": "Buscar sessões...", @@ -1262,9 +1264,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Não resumir conteúdo colado grande", "settings.experimental.batch.title": "Ferramenta em lote", "settings.experimental.batch.description": "Ativar processamento em lote de chamadas de ferramentas", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Pesquisa de código", "settings.experimental.codebaseSearch.description": "Ativar pesquisa por linguagem natural com IA em toda a base de código", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 66990e3da96..11ba35c9693 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1030,6 +1030,8 @@ export const dict = { "session.delete.confirm": 'Izbriši sesiju "{{name}}"?', "session.delete.button": "Izbriši sesiju", "session.untitled": "Bez naslova", + "session.current": "Trenutna sesija", + "session.history.sources": "Izvor historije", "session.recent": "Nedavne", "session.showHistory": "Prikaži historiju", "session.search.placeholder": "Pretraži sesije...", @@ -1263,9 +1265,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Ne sažimaj veliki zalijepljeni sadržaj", "settings.experimental.batch.title": "Batch alat", "settings.experimental.batch.description": "Omogući batch obradu poziva alata", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Pretraga koda", "settings.experimental.codebaseSearch.description": "Omogući AI pretragu prirodnim jezikom kroz bazu koda", "settings.experimental.speechToText.title": "Govor u tekst", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index e377a75863e..6b42d0f6efe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1022,6 +1022,8 @@ export const dict = { "session.delete.confirm": 'Slet session "{{name}}"?', "session.delete.button": "Slet session", "session.untitled": "Unavngivet", + "session.current": "Aktuel session", + "session.history.sources": "Historikkilde", "session.recent": "Seneste", "session.showHistory": "Vis historik", "session.search.placeholder": "Søg sessioner...", @@ -1256,9 +1258,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Resumér ikke stort indsat indhold", "settings.experimental.batch.title": "Batchværktøj", "settings.experimental.batch.description": "Aktiver batchbehandling af flere værktøjskald", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Kodesøgning", "settings.experimental.codebaseSearch.description": "Aktiver AI-drevet naturlig sprogsøgning på tværs af kodebasen", "settings.experimental.speechToText.title": "Tale til tekst", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 346bf456ba7..a031734dea3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1037,6 +1037,8 @@ export const dict = { "session.delete.confirm": 'Sitzung "{{name}}" löschen?', "session.delete.button": "Sitzung löschen", "session.untitled": "Unbenannt", + "session.current": "Aktuelle Sitzung", + "session.history.sources": "Quelle des Verlaufs", "session.recent": "Kürzlich", "session.showHistory": "Verlauf anzeigen", "session.search.placeholder": "Sitzungen suchen...", @@ -1277,9 +1279,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Große eingefügte Inhalte nicht zusammenfassen", "settings.experimental.batch.title": "Batch-Werkzeug", "settings.experimental.batch.description": "Bündelung mehrerer Werkzeugaufrufe aktivieren", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Codebase-Suche", "settings.experimental.codebaseSearch.description": "KI-gestützte Suche in natürlicher Sprache über die gesamte Codebasis aktivieren", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 159e3634d83..b74a4d86714 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -948,8 +948,10 @@ export const dict = { "session.delete.confirm": 'Delete session "{{name}}"?', "session.delete.button": "Delete session", "session.untitled": "Untitled", + "session.current": "Current session", "session.recent": "Recent", "session.showHistory": "Show History", + "session.history.sources": "History source", "session.search.placeholder": "Search sessions...", "session.empty": "No sessions yet. Click + to start a new conversation.", "session.tab.local": "Local", @@ -1243,9 +1245,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Don't summarize large pasted content", "settings.experimental.batch.title": "Batch Tool", "settings.experimental.batch.description": "Enable batching of multiple tool calls", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Codebase Search", "settings.experimental.codebaseSearch.description": "Enable AI-powered natural language search across your codebase", "settings.experimental.speechToText.title": "Speech to Text", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 6d339c64938..39af9949cf7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1032,6 +1032,8 @@ export const dict = { "session.delete.confirm": '¿Eliminar sesión "{{name}}"?', "session.delete.button": "Eliminar sesión", "session.untitled": "Sin título", + "session.current": "Sesión actual", + "session.history.sources": "Origen del historial", "session.recent": "Recientes", "session.showHistory": "Mostrar historial", "session.search.placeholder": "Buscar sesiones...", @@ -1269,9 +1271,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "No resumir contenido pegado grande", "settings.experimental.batch.title": "Herramienta por lotes", "settings.experimental.batch.description": "Habilitar procesamiento por lotes de llamadas a herramientas", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Búsqueda de código", "settings.experimental.codebaseSearch.description": "Habilitar búsqueda por lenguaje natural con IA en toda la base de código", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index bfc477ed721..5a43fe24ed8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1040,6 +1040,8 @@ export const dict = { "session.delete.confirm": 'Supprimer la session "{{name}}" ?', "session.delete.button": "Supprimer la session", "session.untitled": "Sans titre", + "session.current": "Session actuelle", + "session.history.sources": "Source de l'historique", "session.recent": "Récentes", "session.showHistory": "Afficher l'historique", "session.search.placeholder": "Rechercher des sessions...", @@ -1281,9 +1283,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Ne pas résumer le contenu collé volumineux", "settings.experimental.batch.title": "Outil par lot", "settings.experimental.batch.description": "Activer le traitement par lot d'appels d'outils", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Recherche de code", "settings.experimental.codebaseSearch.description": "Activer la recherche en langage naturel par IA dans toute la base de code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 3ce53bc55d3..d0fb5e1f8e4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -841,8 +841,10 @@ export const dict = { "session.delete.confirm": 'Eliminare la sessione "{{name}}"?', "session.delete.button": "Elimina sessione", "session.untitled": "Senza titolo", + "session.current": "Sessione corrente", "session.recent": "Recenti", "session.showHistory": "Mostra cronologia", + "session.history.sources": "Origine cronologia", "session.search.placeholder": "Cerca sessioni...", "session.empty": "Ancora nessuna sessione. Fai clic su + per avviare una nuova conversazione.", "session.tab.local": "Locale", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 3d451e9901b..ed22e53da7e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1016,6 +1016,8 @@ export const dict = { "session.delete.confirm": 'セッション "{{name}}" を削除しますか?', "session.delete.button": "セッションを削除", "session.untitled": "無題", + "session.current": "現在のセッション", + "session.history.sources": "履歴のソース", "session.recent": "最近", "session.showHistory": "履歴を表示", "session.search.placeholder": "セッションを検索...", @@ -1251,9 +1253,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "大量のペーストコンテンツを要約しない", "settings.experimental.batch.title": "バッチツール", "settings.experimental.batch.description": "複数のツール呼び出しのバッチ処理を有効にする", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "コードベース検索", "settings.experimental.codebaseSearch.description": "コードベース全体でAIによる自然言語検索を有効にする", "settings.experimental.speechToText.title": "音声認識", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 4dacfcf2725..df2b11f6cca 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -978,6 +978,8 @@ export const dict = { "session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?', "session.delete.button": "세션 삭제", "session.untitled": "제목 없음", + "session.current": "현재 세션", + "session.history.sources": "기록 출처", "session.recent": "최근", "session.showHistory": "기록 보기", "session.search.placeholder": "세션 검색...", @@ -1245,9 +1247,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "대량 붙여넣기 콘텐츠를 요약하지 않음", "settings.experimental.batch.title": "배치 도구", "settings.experimental.batch.description": "여러 도구 호출의 배치 처리 활성화", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "코드베이스 검색", "settings.experimental.codebaseSearch.description": "코드베이스 전체에서 AI 기반 자연어 검색 활성화", "settings.experimental.speechToText.title": "음성 텍스트 변환", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 0123096dde9..a328eb3b296 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -985,6 +985,8 @@ export const dict = { "session.delete.confirm": 'Sessie "{{name}}" verwijderen?', "session.delete.button": "Verwijder sessie", "session.untitled": "Naamloos", + "session.current": "Huidige sessie", + "session.history.sources": "Geschiedenisbron", "session.recent": "Recent", "session.showHistory": "Geschiedenis weergeven", "session.search.placeholder": "Zoek sessies...", @@ -1258,9 +1260,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Vat grote geplakte inhoud niet samen", "settings.experimental.batch.title": "Batch Tool", "settings.experimental.batch.description": "Schakel batching van meerdere tool calls in", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Codebase Zoeken", "settings.experimental.codebaseSearch.description": "Schakel AI-aangedreven zoeken in natuurlijke taal door je codebase in", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index b6d40dff990..0ac0ea3f2b8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -989,6 +989,8 @@ export const dict = { "session.delete.confirm": 'Slette sesjonen "{{name}}"?', "session.delete.button": "Slett sesjon", "session.untitled": "Uten tittel", + "session.current": "Gjeldende økt", + "session.history.sources": "Historikkilde", "session.recent": "Nylige", "session.showHistory": "Vis historikk", "session.search.placeholder": "Søk i sesjoner...", @@ -1222,9 +1224,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Ikke oppsummer stort limt innhold", "settings.experimental.batch.title": "Batchverktøy", "settings.experimental.batch.description": "Aktiver batchbehandling av verktøykall", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Kodesøk", "settings.experimental.codebaseSearch.description": "Aktiver AI-drevet naturlig språksøk på tvers av kodebasen", "settings.experimental.speechToText.title": "Tale til tekst", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 0baa8fd6897..5b1a0395eec 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -988,6 +988,8 @@ export const dict = { "session.delete.confirm": 'Usunąć sesję "{{name}}"?', "session.delete.button": "Usuń sesję", "session.untitled": "Bez tytułu", + "session.current": "Bieżąca sesja", + "session.history.sources": "Źródło historii", "session.recent": "Ostatnie", "session.showHistory": "Pokaż historię", "session.search.placeholder": "Szukaj sesji...", @@ -1221,9 +1223,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Nie podsumowuj dużego wklejonego tekstu", "settings.experimental.batch.title": "Narzędzie wsadowe", "settings.experimental.batch.description": "Włącz przetwarzanie wsadowe wywołań narzędzi", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Wyszukiwanie kodu", "settings.experimental.codebaseSearch.description": "Włącz wyszukiwanie w języku naturalnym z AI w całej bazie kodu", "settings.experimental.speechToText.title": "Mowa na tekst", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 26f857dc8f3..7bfae609bad 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1027,6 +1027,8 @@ export const dict = { "session.delete.confirm": 'Удалить сессию "{{name}}"?', "session.delete.button": "Удалить сессию", "session.untitled": "Без названия", + "session.current": "Текущая сессия", + "session.history.sources": "Источник истории", "session.recent": "Недавние", "session.showHistory": "Показать историю", "session.search.placeholder": "Поиск сессий...", @@ -1259,9 +1261,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Не суммировать большой вставленный контент", "settings.experimental.batch.title": "Пакетный инструмент", "settings.experimental.batch.description": "Включить пакетную обработку вызовов инструментов", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Поиск по коду", "settings.experimental.codebaseSearch.description": "Включить поиск на естественном языке с ИИ по всей кодовой базе", "settings.experimental.speechToText.title": "Речь в текст", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 919515ba1d0..95d9ab20cee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1013,6 +1013,8 @@ export const dict = { "session.delete.confirm": 'ลบเซสชัน "{{name}}" หรือไม่?', "session.delete.button": "ลบเซสชัน", "session.untitled": "ไม่มีชื่อ", + "session.current": "เซสชันปัจจุบัน", + "session.history.sources": "แหล่งที่มาของประวัติ", "session.recent": "ล่าสุด", "session.showHistory": "แสดงประวัติ", "session.search.placeholder": "ค้นหาเซสชัน...", @@ -1242,9 +1244,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "ไม่สรุปเนื้อหาที่วางขนาดใหญ่", "settings.experimental.batch.title": "เครื่องมือแบทช์", "settings.experimental.batch.description": "เปิดใช้งานการประมวลผลแบทช์ของการเรียกเครื่องมือ", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "ค้นหาโค้ดเบส", "settings.experimental.codebaseSearch.description": "เปิดใช้งานการค้นหาด้วยภาษาธรรมชาติโดย AI ทั่วทั้งโค้ดเบส", "settings.experimental.speechToText.title": "แปลงเสียงเป็นข้อความ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 30b79a52176..a29f816321f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -983,6 +983,8 @@ export const dict = { "session.delete.confirm": '"{{name}}" oturumu silinsin mi?', "session.delete.button": "Oturumu sil", "session.untitled": "Adsız", + "session.current": "Geçerli oturum", + "session.history.sources": "Geçmiş kaynağı", "session.recent": "Son", "session.showHistory": "Geçmişi Göster", "session.search.placeholder": "Oturum ara...", @@ -1251,9 +1253,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Büyük yapıştırılan içeriği özetleme", "settings.experimental.batch.title": "Toplu Araç", "settings.experimental.batch.description": "Birden fazla araç çağrısının toplu işlenmesini etkinleştir", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Kod Tabanı Araması", "settings.experimental.codebaseSearch.description": "Kod tabanınız genelinde yapay zeka destekli doğal dil aramasını etkinleştir", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 1e22d8a327b..5b8c70ff9e8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -984,6 +984,8 @@ export const dict = { "session.delete.confirm": 'Видалити сесію "{{name}}"?', "session.delete.button": "Видалити сесію", "session.untitled": "Без назви", + "session.current": "Поточна сесія", + "session.history.sources": "Джерело історії", "session.recent": "Останні", "session.showHistory": "Показати історію", "session.search.placeholder": "Пошук сесій...", @@ -1251,9 +1253,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "Підсумовувати великий вставлений вміст", "settings.experimental.batch.title": "Пакетний інструмент", "settings.experimental.batch.description": "Увімкнути пакетну обробку кількох викликів інструментів", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "Пошук по кодовій базі", "settings.experimental.codebaseSearch.description": "Увімкнути пошук природною мовою на основі ШІ по всій кодовій базі", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 92c94ed9e0f..02fdefccc83 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -997,6 +997,8 @@ export const dict = { "session.delete.confirm": '删除会话 "{{name}}"?', "session.delete.button": "删除会话", "session.untitled": "无标题", + "session.current": "当前会话", + "session.history.sources": "历史记录来源", "session.recent": "最近", "session.showHistory": "显示历史", "session.search.placeholder": "搜索会话...", @@ -1224,9 +1226,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "不对大量粘贴内容进行摘要", "settings.experimental.batch.title": "批量工具", "settings.experimental.batch.description": "启用多个工具调用的批处理", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "代码库搜索", "settings.experimental.codebaseSearch.description": "启用 AI 驱动的自然语言代码库搜索", "settings.experimental.speechToText.title": "语音转文本", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 5214459dd9f..895c83aab7e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -965,6 +965,8 @@ export const dict = { "session.delete.confirm": '刪除工作階段 "{{name}}"?', "session.delete.button": "刪除工作階段", "session.untitled": "未命名", + "session.current": "目前的工作階段", + "session.history.sources": "歷史記錄來源", "session.recent": "最近", "session.showHistory": "顯示歷史", "session.search.placeholder": "搜尋工作階段...", @@ -1192,9 +1194,6 @@ export const dict = { "settings.experimental.pasteSummary.description": "不對大量貼上內容進行摘要", "settings.experimental.batch.title": "批次工具", "settings.experimental.batch.description": "啟用多個工具呼叫的批次處理", - "settings.experimental.semanticIndexing.title": "Semantic Indexing", - "settings.experimental.semanticIndexing.description": - "Enable semantic codebase indexing and the semantic_search tool. Requires indexing configuration.", "settings.experimental.codebaseSearch.title": "程式碼庫搜尋", "settings.experimental.codebaseSearch.description": "啟用 AI 驅動的自然語言程式碼庫搜尋", "settings.experimental.speechToText.title": "語音轉文字", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index cc27a507228..9fca0126250 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -292,7 +292,7 @@ const ConfigWrapper: ParentComponent<{ config?: Config; onConfigChange?: (config } return { - indexing: hasIndexingPlugin(config.plugin ?? []) && config.experimental?.semantic_indexing === true, + indexing: hasIndexingPlugin(config.plugin ?? []), } }) diff --git a/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx index 751a75f66a6..604416c539d 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/history.stories.tsx @@ -25,6 +25,7 @@ import { dict as uiEn } from "@kilocode/kilo-ui/i18n/en" import { dict as appEn } from "../i18n/en" import { dict as kiloEn } from "@kilocode/kilo-i18n/en" import SessionList from "../components/history/SessionList" +import HistoryView from "../components/history/HistoryView" const dict: Record = { ...appEn, ...uiEn, ...kiloEn } function t(key: string) { @@ -150,12 +151,32 @@ const meta: Meta = { export default meta type Story = StoryObj +const SessionListDemo = () => { + const [selected, setSelected] = createSignal("") + + return ( + +
+ + + {selected()} + +
+
+ ) +} + export const WithItems: Story = { name: "With sessions", + render: () => , +} + +export const Sources: Story = { + name: "Local and cloud sources", render: () => (
- +
), diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index f084e2d025a..8fdfc3cd4e0 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -71,6 +71,17 @@ export const ModelsAutocompleteOpen: Story = { ), } +export const ModelsAccessibleLabels: Story = { + name: "ModelsTab — accessible model labels", + render: () => ( + +
+ +
+
+ ), +} + function OpenModelPicker(props: { children: any }) { let ref: HTMLDivElement | undefined onMount(() => { @@ -408,9 +419,6 @@ export const IndexingProviderBlurRace: Story = { render: () => { const [saved, setSaved] = createSignal>({}) const cfg: Config = { - experimental: { - semantic_indexing: true, - }, indexing: { provider: "openai", model: "text-embedding-3-large", @@ -439,9 +447,6 @@ export const IndexingKiloModelPreset: Story = { name: "IndexingTab - Kilo stale custom model fallback", render: () => { const cfg: Config = { - experimental: { - semantic_indexing: true, - }, indexing: { provider: "kilo", model: "custom/model", @@ -473,9 +478,6 @@ export const IndexingKiloCatalogLoading: Story = { render: () => { const [saved, setSaved] = createSignal>({}) const cfg: Config = { - experimental: { - semantic_indexing: true, - }, indexing: {}, } return ( diff --git a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx index 18f48f1db9c..7474b0809a4 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx @@ -3,9 +3,13 @@ * Stories for shared controls: ModelSelector. */ +import { createSignal } from "solid-js" import type { Meta, StoryObj } from "storybook-solidjs-vite" -import { StoryProviders } from "./StoryProviders" +import { StoryProviders, mockSessionValue } from "./StoryProviders" import { ModelSelectorBase } from "../components/shared/ModelSelector" +import { SessionContext } from "../context/session" +import type { EnrichedModel } from "../context/provider" +import type { ModelSelection } from "../types/messages" const meta: Meta = { title: "Shared", @@ -32,3 +36,58 @@ export const ModelSelectorNoProviders: Story = { ), } + +const ACCESSIBLE_MODELS: EnrichedModel[] = [ + { id: "alpha", name: "Alpha", providerID: "kilo", providerName: "Kilo" }, + { id: "bravo", name: "Bravo", providerID: "kilo", providerName: "Kilo" }, + { id: "charlie", name: "Charlie", providerID: "kilo", providerName: "Kilo" }, +] + +const AccessibleModelSelector = () => { + const [value, setValue] = createSignal({ providerID: "kilo", modelID: "alpha" }) + + return ( +
+ { + setValue(providerID && modelID ? { providerID, modelID } : null) + }} + /> + {value()?.modelID ?? "default"} +
+ ) +} + +export const ModelSelectorAccessible: Story = { + name: "ModelSelector — accessible interaction", + render: () => ( + + + + ), +} + +export const ModelSelectorSelectedFavorite: Story = { + name: "ModelSelector — selected favorite", + render: () => { + const session = { + ...mockSessionValue(), + favoriteModels: () => [{ providerID: "kilo", modelID: "alpha" }], + } + + return ( + + + + + + ) + }, +} diff --git a/packages/kilo-vscode/webview-ui/src/styles/dialogs.css b/packages/kilo-vscode/webview-ui/src/styles/dialogs.css index e6cbfd3efae..5db5a5c719f 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/dialogs.css +++ b/packages/kilo-vscode/webview-ui/src/styles/dialogs.css @@ -12,6 +12,23 @@ gap: 8px; } +.provider-connect-actions { + align-items: center; + flex-wrap: wrap; +} + +.provider-connect-byok { + flex: 1 1 200px; + font-size: var(--kilo-font-size-12); + color: var(--text-weak-base); +} + +.provider-connect-byok-link { + color: var(--vscode-textLink-foreground); + cursor: pointer; + text-decoration: underline; +} + /* Provider Connect Dialog */ .provider-connect-body { font-size: var(--kilo-font-size-13); diff --git a/packages/kilo-vscode/webview-ui/src/styles/history.css b/packages/kilo-vscode/webview-ui/src/styles/history.css index 83be0683e90..e61598222ff 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/history.css +++ b/packages/kilo-vscode/webview-ui/src/styles/history.css @@ -36,14 +36,33 @@ body.vscode-light color: var(--text-on-interactive-base, white); } +.session-list .session-row { + display: flex; + align-items: center; + align-self: stretch; + width: 100%; +} + +.session-list .session-row > [data-slot="list-item"] { + flex: 1; + min-width: 0; + width: auto; +} + +.session-list [data-slot="session-row-editor"] { + display: flex; + width: 100%; + padding: 2px 8px; +} + body.vscode-light .session-list - [data-slot="list-item"][data-active="true"] + .session-row:has([data-slot="list-item"][data-active="true"]) [data-slot="session-row-action"] [data-slot="icon-svg"], body.vscode-light .session-list - [data-slot="list-item"][data-selected="true"] + .session-row:has([data-slot="list-item"][data-selected="true"]) [data-slot="session-row-action"] [data-slot="icon-svg"] { color: var(--text-on-interactive-base, white); @@ -58,9 +77,9 @@ body.vscode-light transition: opacity 0.15s; } -.session-list [data-slot="list-item"]:hover [data-slot="session-row-action"], -.session-list [data-slot="list-item"][data-active="true"] [data-slot="session-row-action"], -.session-list [data-slot="session-row-action"]:focus-within { +.session-list .session-row:hover [data-slot="session-row-action"], +.session-list .session-row:focus-within [data-slot="session-row-action"], +.session-list .session-row:has([data-slot="list-item"][data-active="true"]) [data-slot="session-row-action"] { opacity: 1; } @@ -146,6 +165,10 @@ body.vscode-light flex-direction: column; } +.history-view-content[hidden] { + display: none; +} + /* Cloud Session List (inside History View) */ .cloud-session-list { display: flex; diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index cc8c23f0395..adecd84ceae 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -207,6 +207,18 @@ flex-shrink: 0; } +.model-selector-assistive { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + .model-selector-search-wrapper { display: flex; align-items: center; @@ -280,7 +292,7 @@ outline-offset: -1px; } -.model-selector-group-label:not(:first-child) { +.model-selector-group:not(:first-child) > .model-selector-group-label { border-top: 1px solid var(--border-weak-base, var(--vscode-panel-border)); margin-top: 4px; padding-top: 8px; @@ -305,10 +317,17 @@ margin-left: 2px; } +.model-selector-row { + display: flex; + align-items: center; +} + .model-selector-item { display: flex; align-items: center; gap: 6px; + flex: 1; + min-width: 0; padding: 5px 12px; font-size: var(--kilo-font-size-12); cursor: pointer; @@ -399,12 +418,16 @@ color 0.1s ease; } +.model-selector-row > .model-selector-star { + margin-right: 12px; +} + .model-selector-star [data-component="icon"] { color: inherit; } -.model-selector-item:hover .model-selector-star, -.model-selector-item.selected .model-selector-star, +.model-selector-row:hover .model-selector-star, +.model-selector-row.selected .model-selector-star, .model-selector-star--active { opacity: 1; } @@ -424,6 +447,7 @@ .model-selector-item-select-btn { flex-shrink: 0; + margin-right: 12px; padding: 2px 10px; font-size: var(--kilo-font-size-11); font-weight: 600; @@ -442,11 +466,6 @@ opacity: 1; } -.model-selector-item-select-btn--hidden { - opacity: 0; - pointer-events: none; -} - /* ============================================ Thinking Selector (uses kilo-ui Popover) ============================================ */ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 28497f83da9..bd47ed509b8 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -40,7 +40,6 @@ export interface WatcherConfig { export interface ExperimentalConfig { disable_paste_summary?: boolean batch_tool?: boolean - semantic_indexing?: boolean codebase_search?: boolean speech_to_text_model?: string primary_tools?: string[] diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 48746eb0c9f..742ef72deef 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -74,7 +74,7 @@ function mergeConfigConcatArrays(target: Info, source: Info): Info { function normalizeLoadedConfig(data: unknown, source: string) { if (!isRecord(data)) return data - const copy = { ...data } + const copy = KilocodeConfig.retireIndexingFlag({ ...data }, source) // kilocode_change const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy if (!hadLegacy) return copy delete copy.theme @@ -350,9 +350,6 @@ export const Info = Schema.Struct({ batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }), codebase_search: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI-powered codebase search" }), // kilocode_change // kilocode_change start - semantic_indexing: Schema.optional(Schema.Boolean).annotate({ - description: "Enable semantic codebase indexing and the semantic_search tool", - }), speech_to_text_model: Schema.optional(Schema.String).annotate({ description: "Speech-to-text transcription model ID to use for voice input", }), diff --git a/packages/opencode/src/kilo-sessions/remote-sender.ts b/packages/opencode/src/kilo-sessions/remote-sender.ts index 38a70975b3f..9666789233b 100644 --- a/packages/opencode/src/kilo-sessions/remote-sender.ts +++ b/packages/opencode/src/kilo-sessions/remote-sender.ts @@ -76,6 +76,7 @@ export namespace RemoteSender { readonly list: () => Promise> readonly reply: (input: Permission.ReplyInput) => Promise } + prompt?: (input: SessionPrompt.PromptInput) => Promise } export type Sender = { @@ -97,6 +98,12 @@ export namespace RemoteSender { return AppRuntime.runPromise(Permission.Service.use((svc) => svc.reply(input))) }, } + const prompt = + options.prompt ?? + (async (input: SessionPrompt.PromptInput) => { + const { AppRuntime } = await import("@/effect/app-runtime") + return AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.prompt(input))) + }) const sub = options.subscribe ?? @@ -294,7 +301,7 @@ export namespace RemoteSender { return } dispatchLongRunning(msg, directoryFor(input.data.sessionID), async () => { - await SessionPrompt.prompt(input.data as SessionPrompt.PromptInput) + await prompt(input.data as SessionPrompt.PromptInput) }) return } diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index 71844ca3d06..cbaf9afc362 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -115,6 +115,14 @@ export namespace KilocodeConfig { return stripGlobalIndexing(info) } + export function retireIndexingFlag(info: Record, source: string) { + if (!isRecord(info.experimental) || !("semantic_indexing" in info.experimental)) return info + const experimental = { ...info.experimental } + delete experimental.semantic_indexing + log.warn("ignored retired experimental.semantic_indexing config; use indexing.enabled instead", { path: source }) + return { ...info, experimental } + } + function stripGlobalIndexing(info: Config.Info): Config.Info { // Indexing provider/storage settings can be global, but enablement is exposed separately from project enablement. if (info.indexing?.enabled === undefined) return info diff --git a/packages/opencode/src/kilocode/indexing-feature.ts b/packages/opencode/src/kilocode/indexing-feature.ts index a19b1b316b9..3cb2e6042ce 100644 --- a/packages/opencode/src/kilocode/indexing-feature.ts +++ b/packages/opencode/src/kilocode/indexing-feature.ts @@ -9,7 +9,6 @@ type PluginSpec = string | [string, Record] type ConfigLike = { plugin?: readonly PluginSpec[] | null - experimental?: { semantic_indexing?: boolean } | null } type Req = { @@ -21,7 +20,7 @@ type LogLike = { } export function indexingEnabled(config?: ConfigLike | null): boolean { - return hasIndexingPlugin(config?.plugin ?? []) && config?.experimental?.semantic_indexing === true + return hasIndexingPlugin(config?.plugin ?? []) } export function resolveIndexingPlugin(req: Req, log?: LogLike): string { diff --git a/packages/opencode/src/kilocode/indexing.ts b/packages/opencode/src/kilocode/indexing.ts index 03ccf671b9e..ab0c883de69 100644 --- a/packages/opencode/src/kilocode/indexing.ts +++ b/packages/opencode/src/kilocode/indexing.ts @@ -242,15 +242,6 @@ export namespace KiloIndexing { return track(hit, await inert(() => missing())) } - if (cfg.experimental?.semantic_indexing !== true) { - return track( - hit, - await inert(() => - disabledIndexingStatus("Semantic indexing is disabled. Enable it in the Experimental settings."), - ), - ) - } - if (isWorktreePath(dir)) { return track(hit, await inert(() => worktreeDisabled())) } diff --git a/packages/opencode/src/kilocode/tool/agent-manager.txt b/packages/opencode/src/kilocode/tool/agent-manager.txt index d1a85145af5..3c3c59bb4b3 100644 --- a/packages/opencode/src/kilocode/tool/agent-manager.txt +++ b/packages/opencode/src/kilocode/tool/agent-manager.txt @@ -10,4 +10,6 @@ Each task may provide a prompt, a short display name, and a branch name. Keep di By default, multiple tasks are started as independent Agent Manager sessions. Set `versions` to true only when all tasks are alternate versions of the same work that should be compared together. Versioned worktrees are grouped in Agent Manager and branch names may receive version suffixes. +If available, use `kilo_local_recall` only if you need context from a completed Agent Manager session. + Do not use this for ordinary subagent research. Use the `task` tool for internal subagents, and use this only when the user wants visible Agent Manager sessions in the extension. diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d411db4e3ad..b7e83bf9f92 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -13,7 +13,6 @@ import { SessionID, MessageID, PartID } from "./schema" import { MessageV2 } from "./message-v2" import * as Log from "@opencode-ai/core/util/log" import { SessionRevert } from "./revert" -import { makeRuntime } from "@/effect/run-service" // kilocode_change import * as Session from "./session" import { Agent } from "../agent/agent" import { Provider } from "@/provider/provider" @@ -2165,11 +2164,4 @@ const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi const placeholderRegex = /\$(\d+)/g const quoteTrimRegex = /^["']|["']$/g -// kilocode_change start - legacy promise helpers for Kilo callsites -const { runPromise } = makeRuntime(Service, defaultLayer) -export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(input)) -export const loop = (input: LoopInput) => runPromise((svc) => svc.loop(input)) -export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID)) -// kilocode_change end - export * as SessionPrompt from "./prompt" diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index cce16404aae..32327bbdc8b 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -54,9 +54,6 @@ async function writeConfig(dir: string, config: object, name = "kilo.json") { const cfg: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { provider: "ollama", vectorStore: "qdrant", @@ -93,6 +90,22 @@ describe("markdown substitutions", () => { }) describe("kilocode indexing config", () => { + test("ignores retired semantic indexing flags in existing configs", async () => { + await using tmp = await tmpdir({ git: true }) + await writeConfig(tmp.path, { + experimental: { semantic_indexing: true, batch_tool: true }, + }) + + await WithInstance.provide({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.experimental?.batch_tool).toBe(true) + expect(config.experimental).not.toHaveProperty("semantic_indexing") + }, + }) + }) + test("keeps global indexing enabled in global config", async () => { await using globalTmp = await tmpdir() await using tmp = await tmpdir() diff --git a/packages/opencode/test/kilocode/indexing-feature.test.ts b/packages/opencode/test/kilocode/indexing-feature.test.ts index 627e0d096f4..1cfde17a0b1 100644 --- a/packages/opencode/test/kilocode/indexing-feature.test.ts +++ b/packages/opencode/test/kilocode/indexing-feature.test.ts @@ -9,12 +9,8 @@ import { describe("indexing plugin helpers", () => { test("detects plugin-enabled configs", () => { expect(indexingEnabled({ plugin: ["global-plugin"] })).toBe(false) - expect(indexingEnabled({ plugin: [INDEXING_PLUGIN] })).toBe(false) - expect(indexingEnabled({ plugin: [INDEXING_PLUGIN], experimental: { semantic_indexing: false } })).toBe(false) - expect(indexingEnabled({ plugin: [INDEXING_PLUGIN], experimental: { semantic_indexing: true } })).toBe(true) - expect( - indexingEnabled({ plugin: ["@kilocode/kilo-indexing@1.0.0"], experimental: { semantic_indexing: true } }), - ).toBe(true) + expect(indexingEnabled({ plugin: [INDEXING_PLUGIN] })).toBe(true) + expect(indexingEnabled({ plugin: ["@kilocode/kilo-indexing@1.0.0"] })).toBe(true) }) test("adds indexing plugin when present but missing from config", () => { diff --git a/packages/opencode/test/kilocode/indexing-startup.test.ts b/packages/opencode/test/kilocode/indexing-startup.test.ts index e90222ee0aa..63c873a994c 100644 --- a/packages/opencode/test/kilocode/indexing-startup.test.ts +++ b/packages/opencode/test/kilocode/indexing-startup.test.ts @@ -16,9 +16,6 @@ const fetch = global.fetch const cfg: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: true, provider: "ollama", @@ -29,13 +26,9 @@ const cfg: Partial = { }, } -const off: Partial = { +const unset: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: false, - }, indexing: { - enabled: true, provider: "ollama", vectorStore: "qdrant", ollama: { @@ -45,9 +38,6 @@ const off: Partial = { } const inactive: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: false, provider: "ollama", @@ -56,9 +46,6 @@ const inactive: Partial = { } const kilo: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: true, vectorStore: "qdrant", @@ -66,9 +53,6 @@ const kilo: Partial = { } const implicitOpenAi: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: true, vectorStore: "qdrant", @@ -79,9 +63,6 @@ const implicitOpenAi: Partial = { } const staleKilo: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: true, provider: "kilo", @@ -303,23 +284,23 @@ describe("indexing startup degradation", () => { } }) - test("stays disabled when semantic indexing flag is off", async () => { - await using tmp = await tmpdir({ git: true, config: off }) + test("stays disabled when indexing enablement is unset", async () => { + await using tmp = await tmpdir({ git: true, config: unset }) process.env["KILO_CONFIG_DIR"] = tmp.path const init = spyOn(CodeIndexManager.prototype, "initialize") await WithInstance.provide({ directory: tmp.path, fn: async () => { - const status = await KiloIndexing.current() + const status = await wait(() => KiloIndexing.current(), "Disabled") expect(status).toMatchObject({ state: "Disabled", - message: "Semantic indexing is disabled. Enable it in the Experimental settings.", + message: "Indexing disabled.", }) expect(await KiloIndexing.available()).toBe(false) expect(KiloIndexing.ready()).toBe(false) - expect(await KiloIndexing.search("flag off")).toEqual([]) + expect(await KiloIndexing.search("disabled")).toEqual([]) expect(init).not.toHaveBeenCalled() }, }) diff --git a/packages/opencode/test/kilocode/indexing-worktree.test.ts b/packages/opencode/test/kilocode/indexing-worktree.test.ts index 0a9a303e505..fa42d92e1be 100644 --- a/packages/opencode/test/kilocode/indexing-worktree.test.ts +++ b/packages/opencode/test/kilocode/indexing-worktree.test.ts @@ -7,9 +7,6 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture" const cfg: Partial = { plugin: ["@kilocode/kilo-indexing"], - experimental: { - semantic_indexing: true, - }, indexing: { enabled: true, provider: "ollama", diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index d65dd8fd9bb..ea6f55eacb0 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -1,4 +1,5 @@ import { describe, expect, spyOn, test } from "bun:test" +import { Effect } from "effect" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { TuiEvent } from "../../src/cli/cmd/tui/event" @@ -1211,10 +1212,16 @@ describe("plan follow-up", () => { expect(followup).toBeDefined() if (!followup) return - expect(states.some((x) => x.sessionID === followup && x.type === "busy")).toBe(true) + const sid = followup + expect(states.some((x) => x.sessionID === sid && x.type === "busy")).toBe(true) const { SessionPrompt } = await import("../../src/session/prompt") - await SessionPrompt.cancel(followup) + await Effect.runPromise( + SessionPrompt.Service.use((svc) => svc.cancel(sid)).pipe( + Effect.provide(SessionPrompt.defaultLayer), + Effect.scoped, + ), + ) deferred.resolve("## Discoveries\n\nexample") await expect(pending).resolves.toBe("break") diff --git a/packages/opencode/test/kilocode/session-prompt-queue.test.ts b/packages/opencode/test/kilocode/session-prompt-queue.test.ts index c07663aff97..9c7b419851a 100644 --- a/packages/opencode/test/kilocode/session-prompt-queue.test.ts +++ b/packages/opencode/test/kilocode/session-prompt-queue.test.ts @@ -13,7 +13,7 @@ import { SessionCompaction } from "../../src/session/compaction" import { SessionPrompt } from "../../src/session/prompt" import { MessageID, SessionID } from "../../src/session/schema" import * as Log from "@opencode-ai/core/util/log" -import { tmpdir } from "../fixture/fixture" +import { provideInstance, tmpdir } from "../fixture/fixture" Log.init({ print: false }) @@ -60,10 +60,20 @@ function reply(input: { text: string; ready?: () => void; wait?: Promise>, text: string) { +function hasText(msg: MessageV2.WithParts, text: string) { return msg.parts.some((part) => part.type === "text" && part.text.includes(text)) } +function scoped(dir: string, fn: (prompt: SessionPrompt.Interface) => Promise) { + return Effect.runPromise( + SessionPrompt.Service.use((prompt) => Effect.promise(() => fn(prompt))).pipe( + Effect.provide(SessionPrompt.defaultLayer), + provideInstance(dir), + Effect.scoped, + ), + ) +} + // Find the last non-system message in an OpenAI-compatible request body. Kept // tolerant: we only care about role invariants, not the exact content shape, // because providers may serialize `content` as a string or as a parts array. @@ -414,77 +424,82 @@ describe("session prompt queue", () => { await WithInstance.provide({ directory: tmp.path, - fn: async () => { - const session = await Session.create({ title: "Queued prompt regression" }) - const first = SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "first prompt" }], - }) + fn: async () => + scoped(tmp.path, async (prompt) => { + const session = await Session.create({ title: "Queued prompt regression" }) + const first = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "first prompt" }], + }), + ) - await ready.promise + await ready.promise - const second = SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "second prompt" }], - }) + const second = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "second prompt" }], + }), + ) - const one = await first - await injected.promise - const two = await second + const one = await first + await injected.promise + const two = await second - expect(calls).toHaveLength(2) + expect(calls).toHaveLength(2) - // The in-flight stream must complete; no aborted error on msg1's reply. - expect(one.info.role).toBe("assistant") - if (one.info.role === "assistant") expect(one.info.error).toBeUndefined() - expect(hasText(one, "first reply")).toBe(true) - expect(hasText(two, "second reply")).toBe(true) + // The in-flight stream must complete; no aborted error on msg1's reply. + expect(one.info.role).toBe("assistant") + if (one.info.role === "assistant") expect(one.info.error).toBeUndefined() + expect(hasText(one, "first reply")).toBe(true) + expect(hasText(two, "second reply")).toBe(true) - const msgs = await Session.messages({ sessionID: session.id }) - const users = msgs.filter((msg) => msg.info.role === "user") - const assistants = msgs.filter((msg) => msg.info.role === "assistant") - const prompts = users.flatMap((msg) => - msg.parts.filter((part) => part.type === "text").map((part) => part.text), - ) - const text = assistants.flatMap((msg) => - msg.parts.filter((part) => part.type === "text").map((part) => part.text), - ) - expect(users).toHaveLength(2) - expect(assistants).toHaveLength(2) - expect(prompts).toContain("first prompt") - expect(prompts).toContain("second prompt") - expect(text).toContain("first reply") - expect(text).toContain("second reply") + const msgs = await Session.messages({ sessionID: session.id }) + const users = msgs.filter((msg) => msg.info.role === "user") + const assistants = msgs.filter((msg) => msg.info.role === "assistant") + const prompts = users.flatMap((msg) => + msg.parts.filter((part) => part.type === "text").map((part) => part.text), + ) + const text = assistants.flatMap((msg) => + msg.parts.filter((part) => part.type === "text").map((part) => part.text), + ) + expect(users).toHaveLength(2) + expect(assistants).toHaveLength(2) + expect(prompts).toContain("first prompt") + expect(prompts).toContain("second prompt") + expect(text).toContain("first reply") + expect(text).toContain("second reply") - const firstUser = users.find((msg) => hasText(msg, "first prompt")) - const secondUser = users.find((msg) => hasText(msg, "second prompt")) - const firstReply = assistants.find((msg) => hasText(msg, "first reply")) - const secondReply = assistants.find((msg) => hasText(msg, "second reply")) - if ( - firstUser?.info.role !== "user" || - secondUser?.info.role !== "user" || - firstReply?.info.role !== "assistant" || - secondReply?.info.role !== "assistant" - ) { - throw new Error("missing expected messages") - } - expect(firstReply.info.parentID).toBe(firstUser.info.id) - expect(secondReply.info.parentID).toBe(secondUser.info.id) + const firstUser = users.find((msg) => hasText(msg, "first prompt")) + const secondUser = users.find((msg) => hasText(msg, "second prompt")) + const firstReply = assistants.find((msg) => hasText(msg, "first reply")) + const secondReply = assistants.find((msg) => hasText(msg, "second reply")) + if ( + firstUser?.info.role !== "user" || + secondUser?.info.role !== "user" || + firstReply?.info.role !== "assistant" || + secondReply?.info.role !== "assistant" + ) { + throw new Error("missing expected messages") + } + expect(firstReply.info.parentID).toBe(firstUser.info.id) + expect(secondReply.info.parentID).toBe(secondUser.info.id) - // Regression for #9492: the second LLM request must end with the - // queued user prompt, not an assistant tail from the prior turn. - // Anthropic's API rejects requests whose final message is assistant - // (prefill), and scope() is supposed to partition the queued target - // turn to the end before the model request is built. - expect(bodies).toHaveLength(2) - const second2 = bodies[1] - expect(JSON.stringify(second2)).toContain("second prompt") - const tail = lastConversational(second2) - expect(tail?.role).toBe("user") - expect(JSON.stringify(tail?.content)).toContain("second prompt") - }, + // Regression for #9492: the second LLM request must end with the + // queued user prompt, not an assistant tail from the prior turn. + // Anthropic's API rejects requests whose final message is assistant + // (prefill), and scope() is supposed to partition the queued target + // turn to the end before the model request is built. + expect(bodies).toHaveLength(2) + const second2 = bodies[1] + expect(JSON.stringify(second2)).toContain("second prompt") + const tail = lastConversational(second2) + expect(tail?.role).toBe("user") + expect(JSON.stringify(tail?.content)).toContain("second prompt") + }), }) } finally { server.stop(true) @@ -531,52 +546,59 @@ describe("session prompt queue", () => { await WithInstance.provide({ directory: tmp.path, - fn: async () => { - const session = await Session.create({ title: "Queued cancel regression" }) - const first = SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "first prompt" }], - }) - await ready.promise + fn: async () => + scoped(tmp.path, async (prompt) => { + const session = await Session.create({ title: "Queued cancel regression" }) + const first = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "first prompt" }], + }), + ) + await ready.promise - const second = SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "second prompt" }], - }) - const third = SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "third prompt" }], - }) + const second = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "second prompt" }], + }), + ) + const third = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "third prompt" }], + }), + ) - // Let msg2/msg3's enqueue capture the current version before cancel bumps it. - await Bun.sleep(20) - expect(calls).toHaveLength(1) + // Let msg2/msg3's enqueue capture the current version before cancel bumps it. + await Bun.sleep(20) + expect(calls).toHaveLength(1) - await SessionPrompt.cancel(session.id) - await Promise.all([first, second, third]) + await Effect.runPromise(prompt.cancel(session.id)) + await Promise.all([first, second, third]) - // The queued prompts must never reach the LLM once cancel flushes the queue. - expect(calls).toHaveLength(1) - const msgs = await Session.messages({ sessionID: session.id }) - const assistants = msgs.filter((msg) => msg.info.role === "assistant") - expect(assistants).toHaveLength(1) - expect(msgs.filter((msg) => msg.info.role === "user")).toHaveLength(3) + // The queued prompts must never reach the LLM once cancel flushes the queue. + expect(calls).toHaveLength(1) + const msgs = await Session.messages({ sessionID: session.id }) + const assistants = msgs.filter((msg) => msg.info.role === "assistant") + expect(assistants).toHaveLength(1) + expect(msgs.filter((msg) => msg.info.role === "user")).toHaveLength(3) - // Internal state should have no lingering tail/version/target entries after the last release. - const ids = await Effect.runPromise( - KiloSessionPromptQueue.enqueue( - session.id, - MessageID.make("message_probe"), - Effect.succeed(KiloSessionPromptQueue.scope(session.id, []).map((item) => item.info.id)), - Effect.succeed([]), - ), - ) - expect(ids).toEqual([]) - expect(KiloSessionPromptQueue.hasFollowup(session.id)).toBe(false) - }, + // Internal state should have no lingering tail/version/target entries after the last release. + const ids = await Effect.runPromise( + KiloSessionPromptQueue.enqueue( + session.id, + MessageID.make("message_probe"), + Effect.succeed(KiloSessionPromptQueue.scope(session.id, []).map((item) => item.info.id)), + Effect.succeed([]), + ), + ) + expect(ids).toEqual([]) + expect(KiloSessionPromptQueue.hasFollowup(session.id)).toBe(false) + }), }) } finally { server.stop(true) @@ -590,41 +612,44 @@ describe("session prompt queue", () => { await WithInstance.provide({ directory: tmp.path, - fn: async () => { - const session = await Session.create({ title: "Suggestion unblock regression" }) - const offShown = Bus.subscribe(Suggestion.Event.Shown, (event) => { - if (event.properties.sessionID === session.id) shown.resolve() - }) - const offDismissed = Bus.subscribe(Suggestion.Event.Dismissed, (event) => { - if (event.properties.sessionID === session.id) dismissed.resolve() - }) - - try { - const base = Suggestion.show({ - sessionID: session.id, - text: "Run review?", - actions: [{ label: "Review", prompt: "/local-review-uncommitted" }], - }).catch((err) => { - if (err instanceof Suggestion.DismissedError) return "dismissed" - throw err + fn: async () => + scoped(tmp.path, async (prompt) => { + const session = await Session.create({ title: "Suggestion unblock regression" }) + const offShown = Bus.subscribe(Suggestion.Event.Shown, (event) => { + if (event.properties.sessionID === session.id) shown.resolve() + }) + const offDismissed = Bus.subscribe(Suggestion.Event.Dismissed, (event) => { + if (event.properties.sessionID === session.id) dismissed.resolve() }) - await shown.promise - await SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "replacement prompt" }], - noReply: true, - }) - await dismissed.promise + try { + const base = Suggestion.show({ + sessionID: session.id, + text: "Run review?", + actions: [{ label: "Review", prompt: "/local-review-uncommitted" }], + }).catch((err) => { + if (err instanceof Suggestion.DismissedError) return "dismissed" + throw err + }) - expect(await base).toBe("dismissed") - expect(await Suggestion.list()).toEqual([]) - } finally { - offShown() - offDismissed() - } - }, + await shown.promise + await Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "replacement prompt" }], + noReply: true, + }), + ) + await dismissed.promise + + expect(await base).toBe("dismissed") + expect(await Suggestion.list()).toEqual([]) + } finally { + offShown() + offDismissed() + } + }), }) }) @@ -635,49 +660,52 @@ describe("session prompt queue", () => { await WithInstance.provide({ directory: tmp.path, - fn: async () => { - const session = await Session.create({ title: "Question unblock regression" }) - const offAsked = Bus.subscribe(Question.Event.Asked, (event) => { - if (event.properties.sessionID === session.id) asked.resolve() - }) - const offRejected = Bus.subscribe(Question.Event.Rejected, (event) => { - if (event.properties.sessionID === session.id) rejected.resolve() - }) - - try { - const pending = Question.ask({ - sessionID: session.id, - questions: [ - { - header: "Continue?", - question: "Should I continue?", - options: [ - { label: "Yes", description: "Go ahead" }, - { label: "No", description: "Stop" }, - ], - }, - ], - }).catch((err) => { - if (err instanceof Question.RejectedError) return "rejected" - throw err + fn: async () => + scoped(tmp.path, async (prompt) => { + const session = await Session.create({ title: "Question unblock regression" }) + const offAsked = Bus.subscribe(Question.Event.Asked, (event) => { + if (event.properties.sessionID === session.id) asked.resolve() + }) + const offRejected = Bus.subscribe(Question.Event.Rejected, (event) => { + if (event.properties.sessionID === session.id) rejected.resolve() }) - await asked.promise - await SessionPrompt.prompt({ - sessionID: session.id, - agent: "code", - parts: [{ type: "text", text: "replacement prompt" }], - noReply: true, - }) - await rejected.promise + try { + const pending = Question.ask({ + sessionID: session.id, + questions: [ + { + header: "Continue?", + question: "Should I continue?", + options: [ + { label: "Yes", description: "Go ahead" }, + { label: "No", description: "Stop" }, + ], + }, + ], + }).catch((err) => { + if (err instanceof Question.RejectedError) return "rejected" + throw err + }) - expect(await pending).toBe("rejected") - expect(await Question.list()).toEqual([]) - } finally { - offAsked() - offRejected() - } - }, + await asked.promise + await Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "replacement prompt" }], + noReply: true, + }), + ) + await rejected.promise + + expect(await pending).toBe("rejected") + expect(await Question.list()).toEqual([]) + } finally { + offAsked() + offRejected() + } + }), }) }) diff --git a/packages/opencode/test/kilocode/sessions/remote-sender.test.ts b/packages/opencode/test/kilocode/sessions/remote-sender.test.ts index 734d974dc5a..bc7210a8f74 100644 --- a/packages/opencode/test/kilocode/sessions/remote-sender.test.ts +++ b/packages/opencode/test/kilocode/sessions/remote-sender.test.ts @@ -4,10 +4,12 @@ import { Effect } from "effect" import { RemoteSender } from "../../../src/kilo-sessions/remote-sender" import type { RemoteWS } from "../../../src/kilo-sessions/remote-ws" import type { RemoteProtocol } from "../../../src/kilo-sessions/remote-protocol" -import { SessionPrompt } from "../../../src/session/prompt" +import type { SessionPrompt } from "../../../src/session/prompt" import { Question } from "../../../src/question" import { Permission } from "../../../src/permission" import { PermissionID } from "../../../src/permission/schema" +import { ModelID, ProviderID } from "../../../src/provider/schema" +import { SessionID } from "../../../src/session/schema" import { Suggestion } from "../../../src/kilocode/suggestion" // kilocode_change function fakeConn() { @@ -55,6 +57,12 @@ function permissions(items: Permission.Request[] = []) { } } +function prompts(calls: SessionPrompt.PromptInput[]) { + return async (input: SessionPrompt.PromptInput) => { + calls.push(input) + } +} + // kilocode_change start afterEach(() => { mock.restore() @@ -321,13 +329,14 @@ describe("RemoteSender", () => { // kilocode_change start test("send_message normalizes string model without prefix", async () => { const { conn, sent } = fakeConn() - const prompt = spyOn(SessionPrompt, "prompt").mockResolvedValue({} as never) + const calls: SessionPrompt.PromptInput[] = [] const sender = RemoteSender.create({ conn, directory: "/tmp/test", log: nolog, subscribe: fakeBus().subscribe, provide: async (input: { directory: string; init?: Effect.Effect; fn: () => R }) => input.fn(), + prompt: prompts(calls), }) sender.handle({ @@ -344,22 +353,25 @@ describe("RemoteSender", () => { await new Promise((r) => setTimeout(r, 0)) expect(sent[0]).toEqual({ type: "response", id: "req_model_string", result: {} }) - expect(prompt).toHaveBeenCalledWith({ - sessionID: "ses_x", - parts: [{ type: "text", text: "hello" }], - model: { providerID: "kilo", modelID: "anthropic/claude-sonnet-4-20250514" }, - }) + expect(calls).toEqual([ + { + sessionID: SessionID.make("ses_x"), + parts: [{ type: "text", text: "hello" }], + model: { providerID: ProviderID.make("kilo"), modelID: ModelID.make("anthropic/claude-sonnet-4-20250514") }, + }, + ]) }) test("send_message keeps kilocode-prefixed model unchanged before internal conversion", async () => { const { conn } = fakeConn() - const prompt = spyOn(SessionPrompt, "prompt").mockResolvedValue({} as never) + const calls: SessionPrompt.PromptInput[] = [] const sender = RemoteSender.create({ conn, directory: "/tmp/test", log: nolog, subscribe: fakeBus().subscribe, provide: async (input: { directory: string; init?: Effect.Effect; fn: () => R }) => input.fn(), + prompt: prompts(calls), }) sender.handle({ @@ -375,11 +387,13 @@ describe("RemoteSender", () => { await new Promise((r) => setTimeout(r, 0)) - expect(prompt).toHaveBeenCalledWith({ - sessionID: "ses_x", - parts: [{ type: "text", text: "hello" }], - model: { providerID: "kilo", modelID: "gpt-5-mini" }, - }) + expect(calls).toEqual([ + { + sessionID: SessionID.make("ses_x"), + parts: [{ type: "text", text: "hello" }], + model: { providerID: ProviderID.make("kilo"), modelID: ModelID.make("gpt-5-mini") }, + }, + ]) }) test("send_message rejects structured model on remote path", () => { @@ -411,13 +425,14 @@ describe("RemoteSender", () => { test("send_message does not special-case kilo-prefixed model", async () => { const { conn, sent } = fakeConn() - const prompt = spyOn(SessionPrompt, "prompt").mockResolvedValue({} as never) + const calls: SessionPrompt.PromptInput[] = [] const sender = RemoteSender.create({ conn, directory: "/tmp/test", log: nolog, subscribe: fakeBus().subscribe, provide: async (input: { directory: string; init?: Effect.Effect; fn: () => R }) => input.fn(), + prompt: prompts(calls), }) sender.handle({ @@ -434,11 +449,13 @@ describe("RemoteSender", () => { await new Promise((r) => setTimeout(r, 0)) expect(sent[0]).toEqual({ type: "response", id: "req_model_kilo", result: {} }) - expect(prompt).toHaveBeenCalledWith({ - sessionID: "ses_x", - parts: [{ type: "text", text: "hello" }], - model: { providerID: "kilo", modelID: "kilo/gpt-5-mini" }, - }) + expect(calls).toEqual([ + { + sessionID: SessionID.make("ses_x"), + parts: [{ type: "text", text: "hello" }], + model: { providerID: ProviderID.make("kilo"), modelID: ModelID.make("kilo/gpt-5-mini") }, + }, + ]) }) // kilocode_change end diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5311a337028..8fa785798ad 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1427,7 +1427,6 @@ export type Config = { disable_paste_summary?: boolean batch_tool?: boolean codebase_search?: boolean - semantic_indexing?: boolean speech_to_text_model?: string openTelemetry?: boolean primary_tools?: Array diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 62c5858362a..cbb96fcc447 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -16798,9 +16798,6 @@ "codebase_search": { "type": "boolean" }, - "semantic_indexing": { - "type": "boolean" - }, "speech_to_text_model": { "type": "string" }, diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts index 2e0b10778ad..49181f613f4 100644 --- a/script/check-opencode-promise-facades.ts +++ b/script/check-opencode-promise-facades.ts @@ -25,7 +25,6 @@ const allow: Record = { "installation/index.ts": "existing installation facade outside #10655", "question/index.ts": "transitional facade deferred for upstream reconciliation in #10655", "session/compaction.ts": "existing compaction facade outside #10655", - "session/prompt.ts": "transitional facade tracked by #10655", "session/session.ts": "transitional facade tracked by #10655", "sync/index.ts": "sync event runtime boundary", }