diff --git a/.changeset/agent-manager-modal-mode-shortcut.md b/.changeset/agent-manager-modal-mode-shortcut.md new file mode 100644 index 00000000000..4efdaa9bc43 --- /dev/null +++ b/.changeset/agent-manager-modal-mode-shortcut.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager mode shortcuts in the New Worktree dialog so the selected mode and its matching model stay in sync. diff --git a/.changeset/bright-files-count.md b/.changeset/bright-files-count.md new file mode 100644 index 00000000000..84e4aa4fa2b --- /dev/null +++ b/.changeset/bright-files-count.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show aggregate added and removed line counts for multi-file patch tool calls. diff --git a/.changeset/compaction-threshold-boundary.md b/.changeset/compaction-threshold-boundary.md new file mode 100644 index 00000000000..2e610dbefb0 --- /dev/null +++ b/.changeset/compaction-threshold-boundary.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent configured compaction thresholds from interrupting active tool sequences. diff --git a/.changeset/explain-tool-auto-approval-tui.md b/.changeset/explain-tool-auto-approval-tui.md new file mode 100644 index 00000000000..df3e2f5cd03 --- /dev/null +++ b/.changeset/explain-tool-auto-approval-tui.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Show why a tool call was auto-approved or denied in the TUI, and record the denial reason on the tool call metadata (visible in `kilo export`) alongside the existing auto-approval reason. diff --git a/.changeset/fast-local-recall.md b/.changeset/fast-local-recall.md new file mode 100644 index 00000000000..a2cc81e647c --- /dev/null +++ b/.changeset/fast-local-recall.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Speed up local session recall searches across large conversation histories. diff --git a/.changeset/instant-worktree-hover.md b/.changeset/instant-worktree-hover.md new file mode 100644 index 00000000000..e95e1af87ba --- /dev/null +++ b/.changeset/instant-worktree-hover.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show and hide Agent Manager worktree hover cards instantly. diff --git a/.changeset/prompt-training-indicator-height.md b/.changeset/prompt-training-indicator-height.md new file mode 100644 index 00000000000..e1842cbfef4 --- /dev/null +++ b/.changeset/prompt-training-indicator-height.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the prompt controls at a consistent height when the model selector shows the prompt-training indicator. diff --git a/.changeset/quiet-metadata-generators.md b/.changeset/quiet-metadata-generators.md new file mode 100644 index 00000000000..67bb87bcbd9 --- /dev/null +++ b/.changeset/quiet-metadata-generators.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep Kilo's persona out of generated conversation titles and Agent Manager branch names. diff --git a/.changeset/skill-shell-inline-docs.md b/.changeset/skill-shell-inline-docs.md new file mode 100644 index 00000000000..a0e1e864dfb --- /dev/null +++ b/.changeset/skill-shell-inline-docs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Stop treating `` !`cmd` `` shown as an inline code example in skill documentation as a live command, so it no longer triggers a shell permission prompt. diff --git a/.changeset/vscode-skills-settings-narrow-overflow.md b/.changeset/vscode-skills-settings-narrow-overflow.md new file mode 100644 index 00000000000..bbdb87be496 --- /dev/null +++ b/.changeset/vscode-skills-settings-narrow-overflow.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix skill folder path and URL rows clipping and pushing the remove (×) button off-screen in narrow Skills settings panels. Long paths and URLs now truncate within their row, and hovering a truncated value shows the full path or URL in a tooltip. diff --git a/packages/core/schema.json b/packages/core/schema.json index 38f9ce65c83..3243c24ee6f 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -1,8 +1,10 @@ { "version": "7", "dialect": "sqlite", - "id": "169a0f0f-d58f-479f-b024-fa1c7b9a09db", - "prevIds": ["abd2f920-b822-49af-b8a7-2e48367d424f"], + "id": "7a2d751a-b26c-4a89-9905-800e3f6a8b8a", + "prevIds": [ + "169a0f0f-d58f-479f-b024-fa1c7b9a09db" + ], "ddl": [ { "name": "workspace", @@ -1511,9 +1513,13 @@ "table": "session_share" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1522,9 +1528,13 @@ "table": "workspace" }, { - "columns": ["active_account_id"], + "columns": [ + "active_account_id" + ], "tableTo": "account", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "SET NULL", "nameExplicit": false, @@ -1533,9 +1543,13 @@ "table": "account_state" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], + "columnsTo": [ + "aggregate_id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1544,9 +1558,13 @@ "table": "event" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1555,9 +1573,13 @@ "table": "permission" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1566,9 +1588,13 @@ "table": "project_directory" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1577,9 +1603,13 @@ "table": "message" }, { - "columns": ["message_id"], + "columns": [ + "message_id" + ], "tableTo": "message", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1588,9 +1618,13 @@ "table": "part" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1599,9 +1633,13 @@ "table": "session_context_epoch" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1610,9 +1648,13 @@ "table": "session_input" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1621,9 +1663,13 @@ "table": "session_message" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1632,9 +1678,13 @@ "table": "session" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1643,9 +1693,13 @@ "table": "todo" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1654,133 +1708,174 @@ "table": "session_share" }, { - "columns": ["email", "url"], + "columns": [ + "email", + "url" + ], "nameExplicit": false, "name": "control_account_pk", "entityType": "pks", "table": "control_account" }, { - "columns": ["project_id", "directory"], + "columns": [ + "project_id", + "directory" + ], "nameExplicit": false, "name": "project_directory_pk", "entityType": "pks", "table": "project_directory" }, { - "columns": ["session_id", "position"], + "columns": [ + "session_id", + "position" + ], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "workspace_pk", "table": "workspace", "entityType": "pks" }, { - "columns": ["name"], + "columns": [ + "name" + ], "nameExplicit": false, "name": "data_migration_pk", "table": "data_migration", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_state_pk", "table": "account_state", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_pk", "table": "account", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "credential_pk", "table": "credential", "entityType": "pks" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "nameExplicit": false, "name": "event_sequence_pk", "table": "event_sequence", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "event_pk", "table": "event", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_context_epoch_pk", "table": "session_context_epoch", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_input_pk", "table": "session_input", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_message_pk", "table": "session_message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", @@ -1902,6 +1997,36 @@ "entityType": "indexes", "table": "part" }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + }, + { + "value": "message_id", + "isExpression": false + }, + { + "value": "json_extract(\"data\", '$.type')", + "isExpression": true + }, + { + "value": "CASE WHEN json_extract(\"data\", '$.type') = 'text' THEN coalesce(json_extract(\"data\", '$.text'), '') WHEN json_extract(\"data\", '$.type') = 'file' THEN trim(coalesce(json_extract(\"data\", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.path'), '') || ' ' || coalesce(json_extract(\"data\", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(\"data\", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(\"data\", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(\"data\", '$.source.clientName'), '')) ELSE coalesce(json_extract(\"data\", '$.state.error'), '') END", + "isExpression": true + } + ], + "isUnique": false, + "where": "(json_extract(\"part\".\"data\", '$.type') = 'text' AND coalesce(json_extract(\"part\".\"data\", '$.synthetic'), 0) = 0 AND coalesce(json_extract(\"part\".\"data\", '$.ignored'), 0) = 0) OR json_extract(\"part\".\"data\", '$.type') = 'file' OR (json_extract(\"part\".\"data\", '$.type') = 'tool' AND json_extract(\"part\".\"data\", '$.state.status') = 'error')", + "origin": "manual", + "name": "recall_part_search_idx", + "entityType": "indexes", + "table": "part" + }, { "columns": [ { @@ -2098,4 +2223,4 @@ } ], "renames": [] -} +} \ No newline at end of file diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index 16611f9d75b..4c9c3b0229d 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -245,6 +245,7 @@ export default { yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));`) yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`) yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`) yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`) diff --git a/packages/core/src/kilocode/session/recall-part-index.ts b/packages/core/src/kilocode/session/recall-part-index.ts new file mode 100644 index 00000000000..08762280e70 --- /dev/null +++ b/packages/core/src/kilocode/session/recall-part-index.ts @@ -0,0 +1,25 @@ +import { sql } from "drizzle-orm" +import { index, type AnySQLiteColumn } from "drizzle-orm/sqlite-core" + +export namespace RecallPartIndex { + export const createSql = `CREATE INDEX IF NOT EXISTS \`recall_part_search_idx\` ON \`part\` (\`session_id\`,\`id\`,\`message_id\`,json_extract("data", '$.type'),CASE WHEN json_extract("data", '$.type') = 'text' THEN coalesce(json_extract("data", '$.text'), '') WHEN json_extract("data", '$.type') = 'file' THEN trim(coalesce(json_extract("data", '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.path'), '') || ' ' || coalesce(json_extract("data", '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract("data", '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract("data", '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract("data", '$.source.clientName'), '')) ELSE coalesce(json_extract("data", '$.state.error'), '') END) WHERE json_valid("part"."data") AND ((json_extract("part"."data", '$.type') = 'text' AND coalesce(json_extract("part"."data", '$.synthetic'), 0) = 0 AND coalesce(json_extract("part"."data", '$.ignored'), 0) = 0) OR json_extract("part"."data", '$.type') = 'file' OR (json_extract("part"."data", '$.type') = 'tool' AND json_extract("part"."data", '$.state.status') = 'error'));` + + export function make(table: { + session_id: AnySQLiteColumn + id: AnySQLiteColumn + message_id: AnySQLiteColumn + data: AnySQLiteColumn + }) { + return index("recall_part_search_idx") + .on( + table.session_id, + table.id, + table.message_id, + sql`json_extract(${table.data}, '$.type')`, + sql`CASE WHEN json_extract(${table.data}, '$.type') = 'text' THEN coalesce(json_extract(${table.data}, '$.text'), '') WHEN json_extract(${table.data}, '$.type') = 'file' THEN trim(coalesce(json_extract(${table.data}, '$.filename'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.url'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.url'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.path'), '') || ' ' || coalesce(json_extract(${table.data}, '$.source.name'), '') || ' ' || CASE WHEN coalesce(json_extract(${table.data}, '$.source.uri'), '') NOT LIKE 'data:%' THEN coalesce(json_extract(${table.data}, '$.source.uri'), '') ELSE '' END || ' ' || coalesce(json_extract(${table.data}, '$.source.clientName'), '')) ELSE coalesce(json_extract(${table.data}, '$.state.error'), '') END`, + ) + .where( + sql`json_valid(${table.data}) AND ((json_extract(${table.data}, '$.type') = 'text' AND coalesce(json_extract(${table.data}, '$.synthetic'), 0) = 0 AND coalesce(json_extract(${table.data}, '$.ignored'), 0) = 0) OR json_extract(${table.data}, '$.type') = 'file' OR (json_extract(${table.data}, '$.type') = 'tool' AND json_extract(${table.data}, '$.state.status') = 'error'))`, + ) + } +} diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index a1b9be3044e..243feee99c2 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -44,6 +44,24 @@ const Cost = Schema.Struct({ ), }) +// kilocode_change start - models.dev reasoning_options (snatched from upstream +// v1.18.11, #36624): effort tiers, thinking toggles, and token budgets. +const ReasoningOption = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), + }), + Schema.Struct({ + type: Schema.Literal("toggle"), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) +// kilocode_change end + export const Model = Schema.Struct({ id: Schema.String, name: Schema.String, @@ -51,6 +69,7 @@ export const Model = Schema.Struct({ release_date: Schema.String, attachment: Schema.Boolean, reasoning: Schema.Boolean, + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), // kilocode_change temperature: Schema.Boolean, tool_call: Schema.Boolean, interleaved: Schema.optional( diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index e5ff558cc55..9ca1748bd48 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -13,6 +13,7 @@ import { WorkspaceV2 } from "../workspace" import { Timestamps } from "../database/schema.sql" import type { SystemContext } from "../system-context/index" import { AgentV2 } from "../agent" +import { RecallPartIndex } from "../kilocode/session/recall-part-index" // kilocode_change type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id"> type V1MessageData = Omit @@ -93,6 +94,7 @@ export const PartTable = sqliteTable( (table) => [ index("part_message_id_id_idx").on(table.message_id, table.id), index("part_session_idx").on(table.session_id), + RecallPartIndex.make(table), // kilocode_change ], ) diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md index b91950b5ec3..6418c8fae76 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md @@ -58,6 +58,23 @@ The composer stays editable while the agent is working, so you don't have to wai A queued message shows a subtle **Queued** badge on its bubble. The badge clears when the message starts processing or when the queue drains or is cancelled. Queueing works for Cloud Agent sessions and for remote sessions on a connected `kilo remote` CLI instance. +## Attachments in remote sessions + +When you connect the mobile app to a `kilo remote` CLI session, you can share files in both directions. + +### Sending files from your phone to the CLI + +Attach up to **5 files** (each up to **20 MiB**) from your phone to the remote session. The CLI automatically processes them: + +- **Text, images, and PDFs** — the file content is converted to a `data:` URL and handed directly to the model as a file part. The model sees the content as if you had loaded it locally. +- **Other file types** (binaries, archives, etc.) — the file is saved to a per-session scratch directory on the CLI machine. The session transcript shows the saved path, filename, file size, and MIME type. The agent can inspect the file with the `read` tool for text content or shell utilities for binary content. + +Attaching files from the phone is the mobile flow — this is separate from `kilo run --file `, which attaches local files to a local prompt. + +### Receiving files from the CLI on your phone + +While the CLI is connected, the agent can deliver a file to your phone with the `send_file` tool (up to **4 MiB**, remote sessions only). The file appears as a chip on the tool card — tap the chip to open the share sheet and save or forward the file. This tool works only when `kilo remote` is actively connected; it is not available in Cloud Agent sessions. + ## Reviewing GitHub pull requests Open a pull request from a PR link to review it without leaving the app: diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-200-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-200-chromium-linux.png new file mode 100644 index 00000000000..d68385a0c2a --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-200-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50e25bc93fa7d963c9d3a6b71918e623b02ab42a3c638cc2936a02cd2a773e26 +size 5261 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-420-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-420-chromium-linux.png new file mode 100644 index 00000000000..459f3a9ea6e --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/prompt-input/with-prompt-training-420-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ac5b0f48f483ce5fff844aec9316effb5395ee43740b782ebaa9adab7807f59 +size 5690 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-skills-overflow-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-skills-overflow-chromium-linux.png new file mode 100644 index 00000000000..828756c1887 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-skills-overflow-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b026135c2d4e3e4fa6b6fcb1057306f7d1d92e66ebf33949ab31ff0206f0ae04 +size 35914 diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index 3c67c36e2ad..a386323f999 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -238,6 +238,18 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty color: var(--text-weak); } + [data-slot="message-part-tool-changes"] { + display: inline-flex; + align-items: baseline; + flex-shrink: 0; + } + + [data-slot="basic-tool-tool-subtitle"] + [data-slot="message-part-tool-changes"]::before { + content: "·"; + margin-right: 6px; + color: var(--text-weak); + } + [data-slot="basic-tool-tool-title"], [data-slot="basic-tool-tool-subtitle"], [data-slot="basic-tool-tool-arg"], diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index cbb8982989d..dc360fe7ff3 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1844,12 +1844,12 @@ function ToolMetaLine(props: { ) } -function ToolChanges(props: { changes: DiffValue; animate?: boolean }) { +function ToolChanges(props: { changes: DiffValue; animate?: boolean; slot?: string }) { let ref: HTMLDivElement | undefined useToolFade(() => ref, { delay: 0.04, animate: props.animate }) return ( -
+
) @@ -2764,7 +2764,16 @@ ToolRegistry.register({ /> )} - {(text) => } + + {(text) => ( + <> + + file.additions > 0 || file.deletions > 0)}> + + + + )} +
diff --git a/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts new file mode 100644 index 00000000000..5665ea93a3a --- /dev/null +++ b/packages/kilo-vscode/tests/skills-settings-responsive.spec.ts @@ -0,0 +1,118 @@ +import { expect, test, type Locator, type Page } from "@playwright/test" + +const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" +const STORY_ID = "settings--agent-behaviour-skills-overflow" + +const SEEDED_PATH = "/home/user/projects/very-long-directory-name/skills-collection/team-shared" +const SEEDED_PATH_2 = "./relative/path/to/skills/another/very/long/nested/directory" +const SEEDED_URL = "https://example.com/very/long/path/to/skills/registry/index.json?ref=main&token=abc123" +const SEEDED_URL_2 = "https://other.example.org/skills/v2/registry.json?namespace=team&version=latest" + +function overflowFixture(page: Page) { + return page.goto(`/iframe.html?id=${STORY_ID}&viewMode=story&globals=${GLOBALS}`, { + waitUntil: "load", + }) +} + +function cardFor(loc: Locator) { + return loc.locator("xpath=following-sibling::div[@data-component='card'][1]") +} + +async function assertRowContained(row: Locator, card: Locator, label: string) { + const rowBox = await row.boundingBox() + const cardBox = await card.boundingBox() + expect(rowBox, `${label}: row bounding box`).not.toBeNull() + expect(cardBox, `${label}: card bounding box`).not.toBeNull() + expect(rowBox!.width, `${label}: row width <= card width (no horizontal overflow)`).toBeLessThanOrEqual( + cardBox!.width + 1, + ) + expect(rowBox!.x, `${label}: row left edge inside card`).toBeGreaterThanOrEqual(cardBox!.x - 1) + expect(rowBox!.x + rowBox!.width, `${label}: row right edge inside card`).toBeLessThanOrEqual( + cardBox!.x + cardBox!.width + 1, + ) +} + +test.describe("skills settings responsive layout", () => { + test("folder-path and URL rows stay contained and the × button remains visible at a narrow viewport", async ({ + page, + }) => { + await page.setViewportSize({ width: 320, height: 720 }) + await overflowFixture(page) + + const pathsHeader = page.getByRole("heading", { name: "Skill Folder Paths" }) + const urlsHeader = page.getByRole("heading", { name: "Skill URLs" }) + await expect(pathsHeader).toBeVisible() + await expect(urlsHeader).toBeVisible() + + const pathsCard = cardFor(pathsHeader) + const urlsCard = cardFor(urlsHeader) + await expect(pathsCard).toBeVisible() + await expect(urlsCard).toBeVisible() + + for (const seeded of [SEEDED_PATH, SEEDED_PATH_2]) { + const span = page.getByText(seeded, { exact: true }) + await expect(span, `path value visible: ${seeded}`).toBeVisible() + const trigger = span.locator("xpath=ancestor::div[@data-component='tooltip-trigger'][1]") + await expect(trigger, `path Tooltip trigger wraps the value: ${seeded}`).toBeVisible() + const row = trigger.locator("xpath=parent::div") + await assertRowContained(row, pathsCard, `Skill Folder Paths row "${seeded}"`) + + const closeButton = row.locator('[data-icon="close"]') + await expect(closeButton, "× button is visible").toBeVisible() + const btnBox = await closeButton.boundingBox() + const cardBox = await pathsCard.boundingBox() + expect(btnBox, "× button bounding box").not.toBeNull() + expect(btnBox!.x + btnBox!.width, "× button right edge inside card (not pushed off-screen)").toBeLessThanOrEqual( + cardBox!.x + cardBox!.width + 1, + ) + + // The full path is always in the DOM — the ellipsis is visual-only, so + // screen readers read the complete value without any interaction. The + // Tooltip still adds a hover affordance for mouse users so the full + // path is visible without resizing. + await trigger.hover() + const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) + await expect(content, `Kilo Tooltip exposes full path on hover: ${seeded}`).toBeVisible() + } + + for (const seeded of [SEEDED_URL, SEEDED_URL_2]) { + const span = page.getByText(seeded, { exact: true }) + await expect(span, `URL value visible: ${seeded}`).toBeVisible() + const trigger = span.locator("xpath=ancestor::div[@data-component='tooltip-trigger'][1]") + await expect(trigger, `URL Tooltip trigger wraps the value: ${seeded}`).toBeVisible() + const row = trigger.locator("xpath=parent::div") + await assertRowContained(row, urlsCard, `Skill URLs row "${seeded}"`) + + const closeButton = row.locator('[data-icon="close"]') + await expect(closeButton, "× button is visible").toBeVisible() + const btnBox = await closeButton.boundingBox() + const cardBox = await urlsCard.boundingBox() + expect(btnBox, "× button bounding box").not.toBeNull() + expect(btnBox!.x + btnBox!.width, "× button right edge inside card (not pushed off-screen)").toBeLessThanOrEqual( + cardBox!.x + cardBox!.width + 1, + ) + + // The full URL is always in the DOM — the ellipsis is visual-only, so + // screen readers read the complete value without any interaction. The + // Tooltip still adds a hover affordance for mouse users so the full + // URL is visible without resizing. + await trigger.hover() + const content = page.locator('[data-component="tooltip"]').filter({ hasText: seeded }) + await expect(content, `Kilo Tooltip exposes full URL on hover: ${seeded}`).toBeVisible() + } + + for (const [label, card] of [ + ["Skill Folder Paths", pathsCard], + ["Skill URLs", urlsCard], + ] as const) { + const add = card.getByRole("button", { name: "Add", exact: true }) + await expect(add, `Add button visible inside ${label} card`).toBeVisible() + const addBox = await add.boundingBox() + const cardBox = await card.boundingBox() + expect(addBox, "Add button bounding box").not.toBeNull() + expect(addBox!.x + addBox!.width, `Add button right edge inside ${label} card`).toBeLessThanOrEqual( + cardBox!.x + cardBox!.width + 1, + ) + } + }) +}) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-mode-router.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-mode-router.test.ts new file mode 100644 index 00000000000..50f1560b34b --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-mode-router.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "bun:test" +import { createModeRouter } from "../../webview-ui/agent-manager/mode-router" + +describe("Agent Manager mode router", () => { + it("dispatches to the active modal handler and reports consumption", () => { + const router = createModeRouter() + const directions: number[] = [] + + router.register((direction) => directions.push(direction)) + + expect(router.dispatch(1)).toBe(true) + expect(router.dispatch(-1)).toBe(true) + expect(directions).toEqual([1, -1]) + }) + + it("restores normal routing after the modal unregisters", () => { + const router = createModeRouter() + const dispose = router.register(() => undefined) + + dispose() + + expect(router.dispatch(1)).toBe(false) + }) + + it("does not let an old modal cleanup remove a replacement handler", () => { + const router = createModeRouter() + const first = router.register(() => undefined) + const directions: number[] = [] + + router.register((direction) => directions.push(direction)) + first() + + expect(router.dispatch(1)).toBe(true) + expect(directions).toEqual([1]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/markdown-rendered-children.test.ts b/packages/kilo-vscode/tests/unit/markdown-rendered-children.test.ts new file mode 100644 index 00000000000..6f921539ea1 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/markdown-rendered-children.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "bun:test" +import { Window } from "happy-dom" +import { markdownRenderedChildren } from "../../webview-ui/diff-viewer/markdown-rendered-children" + +const window = new Window() + +describe("markdownRenderedChildren", () => { + it("flattens the current renderer block wrapper", () => { + const root = window.document.createElement("div") + root.innerHTML = ` +
+

Heading

+

Paragraph

+
  • Item
+
Cell
+
+ ` + + expect(markdownRenderedChildren(root).map((node) => node.tagName)).toEqual(["H1", "P", "UL", "TABLE"]) + }) + + it("preserves legacy top-level blocks and ignores inserted annotations", () => { + const root = window.document.createElement("div") + root.innerHTML = ` +

Heading

+
+

Paragraph

+
+
+
Quote
+
+ ` + + expect(markdownRenderedChildren(root).map((node) => node.tagName)).toEqual(["H1", "P", "BLOCKQUOTE"]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/session-model-store.test.ts b/packages/kilo-vscode/tests/unit/session-model-store.test.ts index 01f862b9bd3..0c99900dcb1 100644 --- a/packages/kilo-vscode/tests/unit/session-model-store.test.ts +++ b/packages/kilo-vscode/tests/unit/session-model-store.test.ts @@ -3,6 +3,7 @@ import { type ModelStore, type ResolveEnv, applyModel, + getAgentModel, getSessionModel, getSelected, } from "../../webview-ui/src/context/session-model-store" @@ -149,6 +150,22 @@ describe("per-session model selection", () => { }) describe("per-mode model memory", () => { + it("uses remembered model selections for modes without configured models", () => { + const store = { ...emptyStore(), modelSelections: { ask: gpt } } + + expect(getAgentModel(store, env(), "ask")).toEqual(gpt) + }) + + it("ignores stale remembered selections when a configured mode model is user-set", () => { + const configured: ResolveEnv = { + ...env(), + getModeModel: (name) => (name === "code" ? claude : null), + } + const store = { ...emptyStore(), modelSelections: { code: gpt } } + + expect(getAgentModel(store, configured, "code", true)).toEqual(claude) + }) + it("applyModel in a session writes only to sessionOverrides", () => { const store = emptyStore() const result = applyModel(store, "code", claude, "session-a") diff --git a/packages/kilo-vscode/tests/unit/session-variant-store.test.ts b/packages/kilo-vscode/tests/unit/session-variant-store.test.ts index c6c27294b16..a2b678009c3 100644 --- a/packages/kilo-vscode/tests/unit/session-variant-store.test.ts +++ b/packages/kilo-vscode/tests/unit/session-variant-store.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test" import { cycleVariant, + getAgentVariant, getVariant, sessionVariantKeys, sessionVariants, @@ -43,6 +44,13 @@ describe("per-session variant selection", () => { expect(getVariant(store, model, variants, "ask")).toBe("high") }) + it("resolves the effective variant for a mode and model", () => { + const store: Record = {} + store[variantKey(model, "ask")] = "high" + + expect(getAgentVariant(store, model, { variants: { low: {}, high: {} } }, "ask")).toBe("high") + }) + it("carries the pre-submit agent variant into a newly created session", () => { const store: Record = {} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 979679f24ba..5b5849734a5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -76,6 +76,7 @@ import { ProviderShell } from "../src/context/provider-shell" import { ChatView } from "../src/components/chat" import HistoryView from "../src/components/history/HistoryView" import { NewWorktreeDialog } from "./NewWorktreeDialog" +import { createModeRouter } from "./mode-router" import { ProjectList } from "./ProjectList" import { SidebarBody } from "./SidebarBody" import { TabBar } from "./TabBar" @@ -227,6 +228,7 @@ const AgentManagerContent: Component = () => { const session = useSession() const vscode = useVSCode() const dialog = useDialog() + const mode = createModeRouter() let sidebarSearchMenu: SidebarSearchMenuRef | undefined const [kb, setKb] = createSignal>(defaultBindings) @@ -1111,9 +1113,11 @@ const AgentManagerContent: Component = () => { else if (msg.action === "focusSearch") focusChatSearch({ history: setHistory, review: setReviewActive, terminal: () => terms.setActiveId(undefined) }) else if (msg.action === "newTerminal") termHandlers.requestNew() - else if (msg.action === "cycleAgentMode" && document.hasFocus()) cycleAgent(1) - else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) cycleAgent(-1) - else { + else if (msg.action === "cycleAgentMode" && document.hasFocus()) { + if (!mode.dispatch(1)) cycleAgent(1) + } else if (msg.action === "cyclePreviousAgentMode" && document.hasFocus()) { + if (!mode.dispatch(-1)) cycleAgent(-1) + } else { // Handle jumpTo1 through jumpTo9 const match = /^jumpTo([1-9])$/.exec(msg.action ?? "") if (match) projectNav.jump(parseInt(match[1]!) - 1) @@ -1790,7 +1794,9 @@ const AgentManagerContent: Component = () => { const showNewWorktreeDialog = () => { if (!loaded()) return expandSidebar() - dialog.show(() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />) + dialog.show(() => ( + dialog.close()} defaultBaseBranch={repoDefaultBranch()} /> + )) } const confirmDeleteWorktree = (worktreeId: string) => { @@ -2256,6 +2262,7 @@ const AgentManagerContent: Component = () => { selectedProject={activeProjectId()} selection={selection() ?? undefined} currentSessionID={session.currentSessionID} + mode={mode} bindings={kb()} t={t} onSearchRef={(ref) => (sidebarSearchMenu = ref)} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index a91193c725a..65217564e36 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -44,6 +44,8 @@ import { insertSpacedText } from "../src/components/chat/prompt-input-utils" import { WandSparkles } from "@kilocode/kilo-ui/lucide" import { BranchSelect, BranchSelectPopover } from "../src/components/shared/BranchSelect" import { tracker } from "./telemetry" +import { cycleAgent } from "../src/context/session-agent" +import type { ModeRouter } from "./mode-router" type VersionCount = 1 | 2 | 3 | 4 const VERSION_OPTIONS: VersionCount[] = [1, 2, 3, 4] @@ -74,9 +76,12 @@ function sanitizeBranchName(name: string): string { .join("/") } -export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBranch?: string; projectId?: string }> = ( - props, -) => { +export const NewWorktreeDialog: Component<{ + onClose: () => void + defaultBaseBranch?: string + projectId?: string + mode: ModeRouter +}> = (props) => { const { t } = useLanguage() const vscode = useVSCode() const server = useServer() @@ -101,10 +106,12 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const cached = vscode.getState>() const [prompt, setPrompt] = createSignal((cached?.advancedDialogPrompt as string) ?? "") const [versions, setVersions] = createSignal(1) - const [model, setModel] = createSignal<{ providerID: string; modelID: string } | null>(session.configModel()) + const initialAgent = session.selectedAgent() + const initialModel = session.modelForAgent(initialAgent) + const [model, setModel] = createSignal<{ providerID: string; modelID: string } | null>(initialModel) const [compareMode, setCompareMode] = createSignal(false) const [modelAllocations, setModelAllocations] = createSignal(new Map()) - const [agent, setAgent] = createSignal(session.selectedAgent()) + const [agent, setAgent] = createSignal(initialAgent) const [starting, setStarting] = createSignal(false) const [enhancing, setEnhancing] = createSignal(false) const [showAdvanced, setShowAdvanced] = createSignal(false) @@ -113,7 +120,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [baseBranchOpen, setBaseBranchOpen] = createSignal(false) const [compareOpen, setCompareOpen] = createSignal(false) const [highlightedIndex, setHighlightedIndex] = createSignal(0) - const [variant, setVariant] = createSignal(session.currentVariant()) + const [variant, setVariant] = createSignal(session.variantForAgent(initialAgent, initialModel)) const [sandbox, setSandbox] = createSignal() const [sandboxDefault, setSandboxDefault] = createSignal() const [sandboxOverride, setSandboxOverride] = createSignal() @@ -133,6 +140,34 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran setEnhancing(false) } + const selectAgent = (name: string) => { + setAgent(name) + const sel = session.modelForAgent(name) + setModel(sel) + setVariant(session.variantForAgent(name, sel)) + } + + const resetModel = () => { + const sel = session.configModelForAgent(agent()) + setModel(sel) + setVariant(session.variantForAgent(agent(), sel)) + } + + const cycle = (direction: 1 | -1) => { + cycleAgent({ + agents: session.agents(), + direction, + selected: () => agent(), + select: selectAgent, + }) + } + + createEffect(() => { + if (tab() !== "new") return + const dispose = props.mode.register(cycle) + onCleanup(dispose) + }) + // Variant list for the currently selected model const variants = createMemo(() => { const sel = model() @@ -153,7 +188,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran // True when the user has changed the model from the session/config default const overridden = createMemo(() => { const sel = model() - const cfg = session.configModel() + const cfg = session.configModelForAgent(agent()) if (!sel || !cfg) return false return sel.providerID !== cfg.providerID || sel.modelID !== cfg.modelID }) @@ -583,7 +618,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran @@ -611,7 +646,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran