merge: sync main into read permission fix

This commit is contained in:
marius-kilocode
2026-07-13 18:36:01 +02:00
1292 changed files with 106211 additions and 35172 deletions
@@ -90,9 +90,9 @@ Within each file, migrate git paths first where applicable.
Migrate git-centric call sites to `Process.git*` helpers:
- `src/file/index.ts`
- `../core/src/filesystem.ts`
- `src/project/vcs.ts`
- `src/file/watcher.ts`
- `../core/src/filesystem/watcher.ts`
- `src/storage/storage.ts`
- `src/cli/cmd/pr.ts`
@@ -102,7 +102,7 @@ Migrate residual non-git usages:
- `src/cli/cmd/tui/util/clipboard.ts`
- `src/util/archive.ts`
- `src/file/ripgrep.ts`
- `../core/src/filesystem/ripgrep.ts`
- `src/tool/bash.ts`
- `src/cli/cmd/uninstall.ts`
+59
View File
@@ -1,5 +1,64 @@
# @kilocode/cli
## 7.4.7
## 7.4.6
### Minor Changes
- [#12075](https://github.com/Kilo-Org/kilocode/pull/12075) [`1e0b25a`](https://github.com/Kilo-Org/kilocode/commit/1e0b25a134a11c03494d5871be3e43a6881f1d87) - Support configuring network destinations that sandboxed tools can reach while network access is otherwise restricted.
### Patch Changes
- [#12073](https://github.com/Kilo-Org/kilocode/pull/12073) [`71aa54e`](https://github.com/Kilo-Org/kilocode/commit/71aa54e4131a9ac9b39d2d9585b2101da76d35ca) - Inherit the current model and reasoning variant when Agent Manager starts sessions without explicit overrides.
- [#12166](https://github.com/Kilo-Org/kilocode/pull/12166) [`4618f1b`](https://github.com/Kilo-Org/kilocode/commit/4618f1b092a948459374a733625f06d02447dc6e) - Preserve dynamic tool properties when removing unsupported regex lookarounds.
- [#12164](https://github.com/Kilo-Org/kilocode/pull/12164) [`039b73d`](https://github.com/Kilo-Org/kilocode/commit/039b73dfaefe93452501a48914eaeeb2f83c572b) - Wait for the primary codebase index before indexing a linked worktree, preventing large worktrees from consuming excessive CPU during startup.
- [#12106](https://github.com/Kilo-Org/kilocode/pull/12106) [`b6b55d1`](https://github.com/Kilo-Org/kilocode/commit/b6b55d1a3454bc057ddd24144b0f8d21f870ee55) - Make session model usage easier to scan with collapsible summary rows and aligned steps and cost columns.
- [#12093](https://github.com/Kilo-Org/kilocode/pull/12093) [`8b46601`](https://github.com/Kilo-Org/kilocode/commit/8b466010c58497acd35867c8a67292c063f3dac4) - Speed up VS Code settings saves by draining pending prompts and disposing worktree instances concurrently.
- [#12079](https://github.com/Kilo-Org/kilocode/pull/12079) [`0a64070`](https://github.com/Kilo-Org/kilocode/commit/0a640706adcf15968ebc5436e83c6a9c5b8cc4ad) - Resolve AWS Bedrock credentials from SSO profiles in packaged CLI builds.
- [#12101](https://github.com/Kilo-Org/kilocode/pull/12101) [`bf2b33b`](https://github.com/Kilo-Org/kilocode/commit/bf2b33b87bfc5c35de2173ea66c50e630458e2a5) Thanks [@Githubguy132010](https://github.com/Githubguy132010)! - Use the correct `filePath` argument name in the Gemini system prompt.
- [#12149](https://github.com/Kilo-Org/kilocode/pull/12149) [`05dadaa`](https://github.com/Kilo-Org/kilocode/commit/05dadaaaed29a04c93aa25f85bddea73a155139e) Thanks [@umi008](https://github.com/umi008)! - Fix Gemma 4 models failing with "thinkingLevel not supported" when using Google AI Studio.
- [#12148](https://github.com/Kilo-Org/kilocode/pull/12148) [`77f7983`](https://github.com/Kilo-Org/kilocode/commit/77f7983995bcf52debe03ed9209dc56ba3153c31) Thanks [@umi008](https://github.com/umi008)! - Install the latest stable CLI release when newer non-CLI or prerelease releases exist.
- [#12167](https://github.com/Kilo-Org/kilocode/pull/12167) [`988a92e`](https://github.com/Kilo-Org/kilocode/commit/988a92eae99e453f5a4fe260b0894d93b7271de9) - Fix `kilo upgrade` for curl installs resolving the wrong latest version
The upgrade command's version resolution for curl-detected installations used GitHub's `/releases/latest` endpoint, which now returns JetBrains plugin releases (e.g. `jetbrains/v7.0.4`) instead of the latest CLI release. This caused `kilo upgrade` to fail for curl installs. Version resolution now uses the npm `latest` dist-tag, matching the install script fix.
- [#11837](https://github.com/Kilo-Org/kilocode/pull/11837) [`654e10e`](https://github.com/Kilo-Org/kilocode/commit/654e10e25b320fc4518dec192e3fb63137b47182) Thanks [@mjnaderi](https://github.com/mjnaderi)! - Show the Kilo Gateway rate-limit message when login has too many pending authorization requests.
- [#12162](https://github.com/Kilo-Org/kilocode/pull/12162) [`3ee9144`](https://github.com/Kilo-Org/kilocode/commit/3ee91448eeadf353fc611d8e42ac1f5c8cb5eac0) - Show troubleshooting and migration guidance when Google Gemini rejects API credentials.
- [#11955](https://github.com/Kilo-Org/kilocode/pull/11955) [`cac82a3`](https://github.com/Kilo-Org/kilocode/commit/cac82a36cac448154c880a0ebdfd283b89559668) Thanks [@jstar0](https://github.com/jstar0)! - Prevent Gemini requests from failing when MCP tool schemas contain `required` fields without matching object properties.
- [#12153](https://github.com/Kilo-Org/kilocode/pull/12153) [`be15cf4`](https://github.com/Kilo-Org/kilocode/commit/be15cf4b556bea96aaef6de1b3c405b86c0d1a6c) - Allow GPT-5.6 models to use tools whose JSON schemas contain regex lookarounds.
- [#12168](https://github.com/Kilo-Org/kilocode/pull/12168) [`032f3bb`](https://github.com/Kilo-Org/kilocode/commit/032f3bb55f85ce2b2cc07cea54edf59b23abfcc4) - Block environment and out-of-project file substitutions in project markdown configuration.
- [#12040](https://github.com/Kilo-Org/kilocode/pull/12040) [`93c209b`](https://github.com/Kilo-Org/kilocode/commit/93c209bfd1f068b26b38ac4e9b7237d4c7f095e1) Thanks [@rakshith1928](https://github.com/rakshith1928)! - Hide gpt-5.5-pro from the model picker when using ChatGPT OAuth login, since Codex rejects it with HTTP 400.
- [#12087](https://github.com/Kilo-Org/kilocode/pull/12087) [`1f99fb2`](https://github.com/Kilo-Org/kilocode/commit/1f99fb2332b398f8f5066587c970454e7c9d49f9) - Stop explicitly directing GPT and Codex models to delegate tasks to subagents.
- [#12105](https://github.com/Kilo-Org/kilocode/pull/12105) [`e0bfed3`](https://github.com/Kilo-Org/kilocode/commit/e0bfed308ce7906e4d9ca923e82eda1c20cefd2b) - Shut down the headless `kilo serve` process automatically when the editor client that launched it exits without a clean signal, preventing orphaned CLI processes.
- [#12092](https://github.com/Kilo-Org/kilocode/pull/12092) [`94b553b`](https://github.com/Kilo-Org/kilocode/commit/94b553b91b130d996ce833e168e579df51a14957) - Show detailed GPT-5.6 reasoning summaries and avoid expandable blank panels when a provider returns only a summary title.
- Updated dependencies [[`039b73d`](https://github.com/Kilo-Org/kilocode/commit/039b73dfaefe93452501a48914eaeeb2f83c572b), [`1e0b25a`](https://github.com/Kilo-Org/kilocode/commit/1e0b25a134a11c03494d5871be3e43a6881f1d87)]:
- @kilocode/kilo-indexing@7.4.6
- @kilocode/sdk@7.5.0
- @kilocode/plugin@7.4.6
- @opencode-ai/ui@7.4.6
- @kilocode/kilo-gateway@7.4.6
- @kilocode/plugin-atomic-chat@7.4.6
- @kilocode/kilo-telemetry@7.4.6
## 7.4.4
### Minor Changes
-10
View File
@@ -1,10 +0,0 @@
import { defineConfig } from "drizzle-kit"
export default defineConfig({
dialect: "sqlite",
schema: "./src/**/*.sql.ts",
out: "./migration",
dbCredentials: {
url: "/home/thdxr/.local/share/kilo/kilo.db",
},
})
@@ -1,90 +0,0 @@
CREATE TABLE `project` (
`id` text PRIMARY KEY,
`worktree` text NOT NULL,
`vcs` text,
`name` text,
`icon_url` text,
`icon_color` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`time_initialized` integer,
`sandboxes` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `message` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `part` (
`id` text PRIMARY KEY,
`message_id` text NOT NULL,
`session_id` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `permission` (
`project_id` text PRIMARY KEY,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY,
`project_id` text NOT NULL,
`parent_id` text,
`slug` text NOT NULL,
`directory` text NOT NULL,
`title` text NOT NULL,
`version` text NOT NULL,
`share_url` text,
`summary_additions` integer,
`summary_deletions` integer,
`summary_files` integer,
`summary_diffs` text,
`revert` text,
`permission` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`time_compacting` integer,
`time_archived` integer,
CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `todo` (
`session_id` text NOT NULL,
`content` text NOT NULL,
`status` text NOT NULL,
`priority` text NOT NULL,
`position` integer NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`),
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE TABLE `session_share` (
`session_id` text PRIMARY KEY,
`id` text NOT NULL,
`secret` text NOT NULL,
`url` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
@@ -1,796 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "068758ed-a97a-46f6-8a59-6c639ae7c20c",
"prevIds": ["00000000-0000-0000-0000-000000000000"],
"ddl": [
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}
@@ -1 +0,0 @@
ALTER TABLE `project` ADD `commands` text;
@@ -1,806 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb",
"prevIds": ["068758ed-a97a-46f6-8a59-6c639ae7c20c"],
"ddl": [
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "commands",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}
@@ -1,11 +0,0 @@
CREATE TABLE `control_account` (
`email` text NOT NULL,
`url` text NOT NULL,
`access_token` text NOT NULL,
`refresh_token` text NOT NULL,
`token_expiry` integer,
`active` integer NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
CONSTRAINT `control_account_pk` PRIMARY KEY(`email`, `url`)
);
@@ -1,897 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "d2736e43-700f-4e9e-8151-9f2f0d967bc8",
"prevIds": ["8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb"],
"ddl": [
{
"name": "control_account",
"entityType": "tables"
},
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "access_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "refresh_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "token_expiry",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "active",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "commands",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}
@@ -1,7 +0,0 @@
CREATE TABLE `workspace` (
`id` text PRIMARY KEY,
`branch` text,
`project_id` text NOT NULL,
`config` text NOT NULL,
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
@@ -1,959 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40",
"prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"],
"ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{
"name": "control_account",
"entityType": "tables"
},
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "config",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "access_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "refresh_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "token_expiry",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "active",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "commands",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_workspace_project_id_project_id_fk",
"entityType": "fks",
"table": "workspace"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}
@@ -1,2 +0,0 @@
ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint
CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`);
@@ -1,983 +0,0 @@
{
"version": "7",
"dialect": "sqlite",
"id": "572fb732-56f4-4b1e-b981-77152c9980dd",
"prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"],
"ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{
"name": "control_account",
"entityType": "tables"
},
{
"name": "project",
"entityType": "tables"
},
{
"name": "message",
"entityType": "tables"
},
{
"name": "part",
"entityType": "tables"
},
{
"name": "permission",
"entityType": "tables"
},
{
"name": "session",
"entityType": "tables"
},
{
"name": "todo",
"entityType": "tables"
},
{
"name": "session_share",
"entityType": "tables"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "config",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "email",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "access_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "refresh_token",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "token_expiry",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "active",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "control_account"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "control_account"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "worktree",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "vcs",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "name",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_url",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "icon_color",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "project"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_initialized",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "sandboxes",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "commands",
"entityType": "columns",
"table": "project"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "message"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "message"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "message_id",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "part"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "part"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "permission"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "data",
"entityType": "columns",
"table": "permission"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "workspace_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "parent_id",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "slug",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "title",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "version",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "share_url",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_additions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_deletions",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_files",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "summary_diffs",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "revert",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "permission",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_compacting",
"entityType": "columns",
"table": "session"
},
{
"type": "integer",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_archived",
"entityType": "columns",
"table": "session"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "content",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "status",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "priority",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "position",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "todo"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "todo"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "secret",
"entityType": "columns",
"table": "session_share"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "url",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_share"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_updated",
"entityType": "columns",
"table": "session_share"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_workspace_project_id_project_id_fk",
"entityType": "fks",
"table": "workspace"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_message_session_id_session_id_fk",
"entityType": "fks",
"table": "message"
},
{
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_part_message_id_message_id_fk",
"entityType": "fks",
"table": "part"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_permission_project_id_project_id_fk",
"entityType": "fks",
"table": "permission"
},
{
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_project_id_project_id_fk",
"entityType": "fks",
"table": "session"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_todo_session_id_session_id_fk",
"entityType": "fks",
"table": "todo"
},
{
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_share_session_id_session_id_fk",
"entityType": "fks",
"table": "session_share"
},
{
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "message_session_idx",
"entityType": "indexes",
"table": "message"
},
{
"columns": [
{
"value": "message_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_message_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "part_session_idx",
"entityType": "indexes",
"table": "part"
},
{
"columns": [
{
"value": "project_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_project_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "workspace_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_workspace_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "parent_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_parent_idx",
"entityType": "indexes",
"table": "session"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "todo_session_idx",
"entityType": "indexes",
"table": "todo"
}
],
"renames": []
}
@@ -1,17 +0,0 @@
CREATE TABLE `account` (
`id` text PRIMARY KEY,
`email` text NOT NULL,
`url` text NOT NULL,
`access_token` text NOT NULL,
`refresh_token` text NOT NULL,
`token_expiry` integer,
`selected_org_id` text,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `account_state` (
`id` integer PRIMARY KEY NOT NULL,
`active_account_id` text,
FOREIGN KEY (`active_account_id`) REFERENCES `account`(`id`) ON UPDATE no action ON DELETE set null
);
File diff suppressed because it is too large Load Diff
@@ -1,5 +0,0 @@
ALTER TABLE `workspace` ADD `type` text NOT NULL;--> statement-breakpoint
ALTER TABLE `workspace` ADD `name` text;--> statement-breakpoint
ALTER TABLE `workspace` ADD `directory` text;--> statement-breakpoint
ALTER TABLE `workspace` ADD `extra` text;--> statement-breakpoint
ALTER TABLE `workspace` DROP COLUMN `config`;
File diff suppressed because it is too large Load Diff
@@ -1,3 +0,0 @@
ALTER TABLE `account_state` ADD `active_org_id` text;--> statement-breakpoint
UPDATE `account_state` SET `active_org_id` = (SELECT `selected_org_id` FROM `account` WHERE `account`.`id` = `account_state`.`active_account_id`);--> statement-breakpoint
ALTER TABLE `account` DROP COLUMN `selected_org_id`;
File diff suppressed because it is too large Load Diff
@@ -1,4 +0,0 @@
DROP INDEX IF EXISTS `message_session_idx`;--> statement-breakpoint
DROP INDEX IF EXISTS `part_message_idx`;--> statement-breakpoint
CREATE INDEX `message_session_time_created_id_idx` ON `message` (`session_id`,`time_created`,`id`);--> statement-breakpoint
CREATE INDEX `part_message_id_id_idx` ON `part` (`message_id`,`id`);
File diff suppressed because it is too large Load Diff
@@ -1,13 +0,0 @@
CREATE TABLE `event_sequence` (
`aggregate_id` text PRIMARY KEY,
`seq` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `event` (
`id` text PRIMARY KEY,
`aggregate_id` text NOT NULL,
`seq` integer NOT NULL,
`type` text NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_event_aggregate_id_event_sequence_aggregate_id_fk` FOREIGN KEY (`aggregate_id`) REFERENCES `event_sequence`(`aggregate_id`) ON DELETE CASCADE
);
File diff suppressed because it is too large Load Diff
@@ -1,16 +0,0 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_workspace` (
`id` text PRIMARY KEY,
`type` text NOT NULL,
`name` text DEFAULT '' NOT NULL,
`branch` text,
`directory` text,
`extra` text,
`project_id` text NOT NULL,
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint
DROP TABLE `workspace`;--> statement-breakpoint
ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
PRAGMA foreign_keys=ON;
File diff suppressed because it is too large Load Diff
@@ -1,13 +0,0 @@
CREATE TABLE `session_entry` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`type` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_session_entry_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
CREATE INDEX `session_entry_session_idx` ON `session_entry` (`session_id`);--> statement-breakpoint
CREATE INDEX `session_entry_session_type_idx` ON `session_entry` (`session_id`,`type`);--> statement-breakpoint
CREATE INDEX `session_entry_time_created_idx` ON `session_entry` (`time_created`);
File diff suppressed because it is too large Load Diff
@@ -1,2 +0,0 @@
ALTER TABLE `project` ADD `icon_url_override` text;
UPDATE `project` SET `icon_url_override` = `icon_url` WHERE `icon_url` IS NOT NULL;
File diff suppressed because it is too large Load Diff
@@ -1,17 +0,0 @@
CREATE TABLE `session_message` (
`id` text PRIMARY KEY,
`session_id` text NOT NULL,
`type` text NOT NULL,
`time_created` integer NOT NULL,
`time_updated` integer NOT NULL,
`data` text NOT NULL,
CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
);
--> statement-breakpoint
DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint
DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint
DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint
CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint
CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint
CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint
DROP TABLE `session_entry`;
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
ALTER TABLE `session` ADD `path` text;
File diff suppressed because it is too large Load Diff
@@ -1,2 +0,0 @@
ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint
ALTER TABLE `session` ADD `model` text;
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
ALTER TABLE `event_sequence` ADD `owner_id` text;
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0;
File diff suppressed because it is too large Load Diff
@@ -1,6 +0,0 @@
ALTER TABLE `session` ADD `cost` real DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `session` ADD `tokens_input` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `session` ADD `tokens_output` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `session` ADD `tokens_reasoning` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `session` ADD `tokens_cache_read` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `session` ADD `tokens_cache_write` integer DEFAULT 0 NOT NULL;
File diff suppressed because it is too large Load Diff
@@ -1,4 +0,0 @@
CREATE TABLE `data_migration` (
`name` text PRIMARY KEY,
`time_completed` integer NOT NULL
);
File diff suppressed because it is too large Load Diff
+23 -16
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.4.5",
"version": "7.4.7",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -13,10 +13,8 @@
"bench:test": "bun run script/bench-test-suite.ts",
"profile:test": "bun run script/profile-test-files.ts",
"build": "bun run script/build.ts",
"fix-node-pty": "bun run script/fix-node-pty.ts",
"dev": "bun run --conditions=browser ./src/index.ts",
"dev:temporary": "bun run --conditions=browser ./src/temporary.ts",
"db": "bun drizzle-kit"
"dev:temporary": "bun run --conditions=browser ./src/temporary.ts"
},
"bin": {
"kilo": "./bin/kilo",
@@ -30,11 +28,6 @@
"bun": "./src/storage/db.bun.ts",
"node": "./src/storage/db.node.ts",
"default": "./src/storage/db.bun.ts"
},
"#pty": {
"bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.bun.ts"
}
},
"devDependencies": {
@@ -68,22 +61,25 @@
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
"@types/npmcli__arborist": "6.3.3",
"@opencode-ai/http-recorder": "workspace:*"
"@opencode-ai/http-recorder": "workspace:*",
"@babel/core": "7.28.4",
"@standard-schema/spec": "1.0.0",
"@types/babel__core": "7.20.5"
},
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/amazon-bedrock": "4.0.112",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.54",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
@@ -94,7 +90,7 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.92",
"@aws-sdk/credential-providers": "3.1025.0",
"@aws-sdk/credential-providers": "3.1057.0",
"@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -170,14 +166,25 @@
"tree-sitter-wasms": "^0.1.12",
"turndown": "7.2.0",
"ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.1",
"venice-ai-sdk-provider": "2.0.2",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"ws": "8.21.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "18.0.0",
"zod": "catalog:"
"zod": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/server": "workspace:*",
"@opentelemetry/context-async-hooks": "2.6.1",
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
"@pierre/diffs": "catalog:",
"@standard-schema/spec": "1.0.0",
"chokidar": "4.0.3",
"glob": "13.0.5",
"minimatch": "10.0.3",
"partial-json": "0.1.7",
"xdg-basedir": "5.1.0"
},
"overrides": {
"drizzle-orm": "catalog:"
+10
View File
@@ -166,6 +166,16 @@ export default {
// },
// },
},
{
filetype: "vue",
wasm: "https://github.com/anomalyco/tree-sitter-vue/releases/download/v0.1.2/tree-sitter-vue.wasm",
queries: {
highlights: [
"https://raw.githubusercontent.com/anomalyco/tree-sitter-vue/v0.1.2/queries/html_tags/highlights.scm",
"https://raw.githubusercontent.com/anomalyco/tree-sitter-vue/v0.1.2/queries/vue/highlights.scm",
],
},
},
{
filetype: "hcl",
wasm: "https://github.com/tree-sitter-grammars/tree-sitter-hcl/releases/download/v1.2.0/tree-sitter-hcl.wasm",
-32
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
@@ -13,36 +12,6 @@ process.chdir(dir)
const generated = await import("./generate.ts")
// Load migrations from migration directories
const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), {
withFileTypes: true,
})
)
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrations = await Promise.all(
migrationDirs.map(async (name) => {
const file = path.join(dir, "migration", name, "migration.sql")
const sql = await Bun.file(file).text()
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
const timestamp = match
? Date.UTC(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6]),
)
: 0
return { sql, timestamp, name }
}),
)
console.log(`Loaded ${migrations.length} migrations`)
await Bun.build({
target: "node",
// kilocode_change start
@@ -57,7 +26,6 @@ await Bun.build({
sourcemap: "linked",
external: ["jsonc-parser", "@lydell/node-pty"],
define: {
KILO_MIGRATIONS: JSON.stringify(migrations),
KILO_MODELS_DEV: generated.modelsData,
KILO_SANDBOX_MUTATION_WORKER_PATH: `'./kilo-sandbox-mutation-worker.js'`, // kilocode_change
KILO_SANDBOX_NETWORK_RELAY_PATH: `'./kilo-sandbox-network-relay.js'`, // kilocode_change
+2 -31
View File
@@ -26,36 +26,6 @@ import { KiloSandboxWorker } from "./kilocode/kilo-sandbox-worker"
import { KiloSandboxNetwork } from "./kilocode/kilo-sandbox-network"
// kilocode_change end
// Load migrations from migration directories
const migrationDirs = (
await fs.promises.readdir(path.join(dir, "migration"), {
withFileTypes: true,
})
)
.filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
.map((entry) => entry.name)
.sort()
const migrations = await Promise.all(
migrationDirs.map(async (name) => {
const file = path.join(dir, "migration", name, "migration.sql")
const sql = await Bun.file(file).text()
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
const timestamp = match
? Date.UTC(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6]),
)
: 0
return { sql, timestamp, name }
}),
)
console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
@@ -330,7 +300,6 @@ for (const item of targets) {
// kilocode_change end
define: {
KILO_VERSION: `'${Script.version}'`,
KILO_MIGRATIONS: JSON.stringify(migrations),
KILO_MODELS_DEV: generated.modelsData,
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
KILO_WORKER_PATH: workerPath,
@@ -347,6 +316,7 @@ for (const item of targets) {
KILO_BWRAP_SHA256: bwrap ? `'${bwrap}'` : "undefined",
KILO_BUILD_KIND: Script.release ? `'release'` : `'source'`,
// kilocode_change end
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
},
})
@@ -432,6 +402,7 @@ for (const item of targets) {
url: "https://github.com/Kilo-Org/kilocode",
},
// kilocode_change end
...(item.abi ? { libc: [item.abi] } : {}),
},
null,
2,
@@ -1,16 +0,0 @@
#!/usr/bin/env bun
import { $ } from "bun"
// drizzle-kit check compares schema to migrations, exits non-zero if drift
const result = await $`bun drizzle-kit check`.quiet().nothrow()
if (result.exitCode !== 0) {
console.error("Schema has changes not captured in migrations!")
console.error("Run: bun drizzle-kit generate")
console.error("")
console.error(result.stderr.toString())
process.exit(1)
}
console.log("Migrations are up to date")
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bun
import fs from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
if (process.platform !== "win32") {
const root = path.join(dir, "node_modules", "node-pty", "prebuilds")
const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper"))
const result = await Promise.all(
files.map(async (file) => {
const stat = await fs.stat(file).catch(() => undefined)
if (!stat) return
if ((stat.mode & 0o111) === 0o111) return
await fs.chmod(file, stat.mode | 0o755)
return file
}),
)
const fixed = result.filter(Boolean)
if (fixed.length) {
console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`)
}
}
@@ -17,8 +17,7 @@ export namespace TestProfile {
"control-plane/workspace.test.ts",
],
filesystem: [
"file/{index,path-traversal,ripgrep,watcher}.test.ts",
"filesystem/filesystem.test.ts",
"filesystem/*.test.ts",
"fixture/fixture.test.ts",
"git/*.test.ts",
"image/*.test.ts",
@@ -33,10 +32,12 @@ export namespace TestProfile {
"kilocode/cli/cmd/serve.test.ts",
"kilocode/cli/install-artifact.test.ts",
"kilocode/config/config.test.ts",
"kilocode/core-watcher.test.ts",
"kilocode/sandbox/*.test.ts",
"kilocode/server/{config-overlay,listener-runtime,tui-config,worktree-list}.test.ts",
"kilocode/session-export/{e2e,sequence,worker,workspace-provider}.test.ts",
"kilocode/session-export/worker/{storage,zstd}.test.ts",
"kilocode/tool/repo_clone.test.ts",
"kilocode/worktree*.test.ts",
],
process: ["session/prompt.test.ts"],
+2 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Schema } from "effect"
import { TuiInfo } from "../src/cli/cmd/tui/config/tui-schema"
@@ -68,7 +69,7 @@ const configFile = process.argv[2]
const tuiFile = process.argv[3]
console.log(configFile)
await Bun.write(configFile, JSON.stringify(generateEffect(Config.Info), null, 2))
await Bun.write(configFile, JSON.stringify(generateEffect(ConfigV1.Info), null, 2))
if (tuiFile) {
console.log(tuiFile)
+4 -4
View File
@@ -32,7 +32,7 @@ Caller-heavy batch, all merged:
1. `src/config/config.ts`
2. `src/provider/provider.ts`
3. `src/file/index.ts`
3. `../core/src/filesystem.ts`
4. `src/lsp/index.ts`
5. `src/mcp/index.ts`
@@ -168,7 +168,7 @@ Usually no.
Prefer the direct form when there is only one expression:
```ts
await AppRuntime.runPromise(File.Service.use((svc) => svc.read(path)))
await Effect.runPromise(FileSystem.Service.use((svc) => svc.read({ path })))
```
Use `Effect.gen(...)` when the workflow actually needs multiple yielded values or branching.
@@ -179,7 +179,7 @@ These were the recurring mistakes and useful corrections from the first two batc
1. Tests should usually provide the specific service layer, not `AppRuntime`.
2. If a test uses `provideTmpdirInstance(...)` and needs child processes, prefer `CrossSpawnSpawner.defaultLayer`.
3. Instance-scoped services may need both the service layer and the right instance fixture. `File` tests, for example, needed `provideInstance(...)` plus `File.defaultLayer`.
3. Location-scoped services may need both the service layer and the right location fixture. `FileSystem` tests, for example, provide `Location.Service` plus `FileSystem.locationLayer`.
4. Do not wrap a single `Service.use(...)` call in `Effect.gen(...)` just to return it. Use the direct form.
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
@@ -201,7 +201,7 @@ Most of the original facade-removal backlog is already done. The practical remai
- [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
- [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
- [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
- [x] `src/file/index.ts` (`File`) - facades removed and merged
- [x] `../core/src/filesystem.ts` (`FileSystem`) - legacy opencode service removed
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
- [x] `src/config/config.ts` (`Config`) - facades removed and merged
+2 -2
View File
@@ -70,7 +70,7 @@ mutable `Flag` or late `process.env` reads.
Tests should vary behavior with explicit layer variants:
```ts
const it = testEffect(MyService.defaultLayer.pipe(Layer.provide(RuntimeFlags.layer({ experimentalScout: true }))))
const it = testEffect(MyService.defaultLayer.pipe(Layer.provide(RuntimeFlags.layer({ experimentalReferences: true }))))
```
Do not mutate `process.env` or `Flag` after services/layers are built.
@@ -179,7 +179,7 @@ Intentional boundaries:
In effectified code, yield existing services instead of dropping to ad hoc
platform APIs.
- Use `AppFileSystem.Service` instead of raw `fs/promises` for app file IO.
- Use `FSUtil.Service` instead of raw `fs/promises` for app file IO.
- Use `AppProcess.Service` instead of direct `ChildProcessSpawner.spawn` or
legacy process helpers.
- Use `HttpClient.HttpClient` instead of raw `fetch` inside Effect code.
+1 -1
View File
@@ -14,7 +14,7 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
- [ ] `config/paths.ts` - split pure helpers from effectful helpers.
Keep `fileInDirectory(...)` as a plain function.
- [ ] `config/paths.ts` - add a `ConfigPaths.Service` for the effectful operations so callers do not inherit `AppFileSystem.Service` directly.
- [ ] `config/paths.ts` - add a `ConfigPaths.Service` for the effectful operations so callers do not inherit `FSUtil.Service` directly.
Initial service surface should cover:
- `projectFiles(...)`
- `directories(...)`
+1 -1
View File
@@ -33,7 +33,7 @@ genuinely outside `AppLayer`.
## Platform Edges
- Use `AppFileSystem.Service` instead of raw filesystem APIs in
- Use `FSUtil.Service` instead of raw filesystem APIs in
effectified services.
- Use `AppProcess.Service` instead of raw process wrappers.
- Use `HttpClient.HttpClient` instead of raw `fetch` in Effect code.
+3 -3
View File
@@ -72,7 +72,7 @@ P6 OA
- `PROC` AppProcess migration — prefer `AppProcess.Service` over raw
process wrappers.
Shrinks: direct spawn callsites and legacy process helpers.
- `FS` AppFileSystem migration — prefer `AppFileSystem.Service` over raw
- `FS` FSUtil migration — prefer `FSUtil.Service` over raw
filesystem APIs.
Shrinks: direct `fs` / `Bun.file` service callsites where inappropriate.
- `RT` Runtime/facade cleanup — remove service-local `makeRuntime`
@@ -172,7 +172,7 @@ Recently completed:
- [x] Built-in websearch provider selection uses the same runtime flags as
tool visibility.
- [x] Removed global default-plugin disabling from test preload.
- [x] `RF-1` Scout reads routed through runtime flags (#27318).
- [x] `RF-1` Reference reads routed through runtime flags (#27318).
- [x] `RF-2` Plan-mode prompt read routed through runtime flags (#27320).
- [x] `RF-3` Event-system reads routed through runtime flags (#27323).
- [x] `RF-4` Workspaces reads routed through runtime flags for session
@@ -229,7 +229,7 @@ Current rules:
## Lower Priority Tracks
- `PROC` / `FS` — continue AppProcess and AppFileSystem migrations as
- `PROC` / `FS` — continue AppProcess and FSUtil migrations as
focused PRs when touching relevant files.
- `RT` — remove service-local runtime facades only when they are not an
intentional boundary.
+3 -3
View File
@@ -11,7 +11,7 @@ The current exported tools in `src/tool` all use `Tool.define(...)` with Effect-
So the remaining work is no longer "convert tools to Effect at all". The remaining work is mostly:
1. remove Promise and raw platform bridges inside individual tool bodies
2. swap tool internals to Effect-native services like `AppFileSystem`, `HttpClient`, and `ChildProcessSpawner`
2. swap tool internals to Effect-native services like `FSUtil`, `HttpClient`, and `ChildProcessSpawner`
3. keep tests and callers aligned with `yield* info.init()` and real service graphs
## Current shape
@@ -67,11 +67,11 @@ Most exported tools are already on the intended Effect-native shape. The remaini
Current spot cleanups worth tracking:
- [x] `read.ts` — streams through `AppFileSystem.Service.stream` with `Stream.splitLines`; the legacy Node stream / `readline` helper is gone
- [x] `read.ts` — streams through `FSUtil.Service.stream` with `Stream.splitLines`; the legacy Node stream / `readline` helper is gone
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
- [x] `patch/index.ts` — apply path now returns `Effect` over `AppFileSystem.Service`; the parser and chunk replacer stay pure
- [x] `patch/index.ts` — apply path now returns `Effect` over `FSUtil.Service`; the parser and chunk replacer stay pure
Notable items that are already effectively on the target path and do not need separate migration bullets right now:
@@ -1,39 +0,0 @@
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
import { type AccessToken, type AccountID, type OrgID, type RefreshToken } from "./schema"
import { Timestamps } from "../storage/schema.sql"
export const AccountTable = sqliteTable("account", {
id: text().$type<AccountID>().primaryKey(),
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<AccessToken>().notNull(),
refresh_token: text().$type<RefreshToken>().notNull(),
token_expiry: integer(),
...Timestamps,
})
export const AccountStateTable = sqliteTable("account_state", {
id: integer().primaryKey(),
active_account_id: text()
.$type<AccountID>()
.references(() => AccountTable.id, { onDelete: "set null" }),
active_org_id: text().$type<OrgID>(),
})
// LEGACY
export const ControlAccountTable = sqliteTable(
"control_account",
{
email: text().notNull(),
url: text().notNull(),
access_token: text().$type<AccessToken>().notNull(),
refresh_token: text().$type<RefreshToken>().notNull(),
token_expiry: integer(),
active: integer({ mode: "boolean" })
.notNull()
.$default(() => false),
...Timestamps,
},
(table) => [primaryKey({ columns: [table.email, table.url] })],
)
+1 -1
View File
@@ -454,6 +454,6 @@ export const layer: Layer.Layer<Service, never, AccountRepo.Service | HttpClient
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(FetchHttpClient.layer))
export * as Account from "./account"
+60 -59
View File
@@ -2,16 +2,13 @@ import { eq } from "drizzle-orm"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db"
import { AccountStateTable, AccountTable } from "./account.sql"
import { Database } from "@opencode-ai/core/database/database"
import { AccountStateTable, AccountTable } from "@opencode-ai/core/account/sql"
import { AccessToken, AccountID, AccountRepoError, Info, OrgID, RefreshToken } from "./schema"
import { normalizeServerUrl } from "./url"
export type AccountRow = (typeof AccountTable)["$inferSelect"]
type DbClient = Parameters<typeof Database.use>[0] extends (db: infer T) => unknown ? T : never
type DbTransactionCallback<A> = Parameters<typeof Database.transaction<A>>[0]
const ACCOUNT_STATE_ID = 1
export interface Interface {
@@ -41,32 +38,24 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Ac
export const use = serviceUse(Service)
export const layer: Layer.Layer<Service> = Layer.effect(
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const { db } = yield* Database.Service
const decode = Schema.decodeUnknownSync(Info)
const query = <A>(f: DbTransactionCallback<A>) =>
Effect.try({
try: () => Database.use(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
})
const query = <A, E>(effect: Effect.Effect<A, E>) =>
effect.pipe(Effect.mapError((cause) => new AccountRepoError({ message: "Database operation failed", cause })))
const tx = <A>(f: DbTransactionCallback<A>) =>
Effect.try({
try: () => Database.transaction(f),
catch: (cause) => new AccountRepoError({ message: "Database operation failed", cause }),
})
const current = (db: DbClient) => {
const state = db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
const current = Effect.fnUntraced(function* () {
const state = yield* db.select().from(AccountStateTable).where(eq(AccountStateTable.id, ACCOUNT_STATE_ID)).get()
if (!state?.active_account_id) return
const account = db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
const account = yield* db.select().from(AccountTable).where(eq(AccountTable.id, state.active_account_id)).get()
if (!account) return
return { ...account, active_org_id: state.active_org_id ?? null }
}
})
const state = (db: DbClient, accountID: AccountID, orgID: Option.Option<OrgID>) => {
const state = (accountID: AccountID, orgID: Option.Option<OrgID>) => {
const id = Option.getOrNull(orgID)
return db
.insert(AccountStateTable)
@@ -79,41 +68,46 @@ export const layer: Layer.Layer<Service> = Layer.effect(
}
const active = Effect.fn("AccountRepo.active")(() =>
query((db) => current(db)).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))),
query(current()).pipe(Effect.map((row) => (row ? Option.some(decode(row)) : Option.none()))),
)
const list = Effect.fn("AccountRepo.list")(() =>
query((db) =>
query(
db
.select()
.from(AccountTable)
.all()
.map((row: AccountRow) => decode({ ...row, active_org_id: null })),
.pipe(Effect.map((rows) => rows.map((row: AccountRow) => decode({ ...row, active_org_id: null })))),
),
)
const remove = Effect.fn("AccountRepo.remove")((accountID: AccountID) =>
tx((db) => {
db.update(AccountStateTable)
.set({ active_account_id: null, active_org_id: null })
.where(eq(AccountStateTable.active_account_id, accountID))
.run()
db.delete(AccountTable).where(eq(AccountTable.id, accountID)).run()
}).pipe(Effect.asVoid),
query(
db.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.update(AccountStateTable)
.set({ active_account_id: null, active_org_id: null })
.where(eq(AccountStateTable.active_account_id, accountID))
.run()
yield* tx.delete(AccountTable).where(eq(AccountTable.id, accountID)).run()
}),
),
).pipe(Effect.asVoid),
)
const use = Effect.fn("AccountRepo.use")((accountID: AccountID, orgID: Option.Option<OrgID>) =>
query((db) => state(db, accountID, orgID)).pipe(Effect.asVoid),
query(state(accountID, orgID)).pipe(Effect.asVoid),
)
const getRow = Effect.fn("AccountRepo.getRow")((accountID: AccountID) =>
query((db) => db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe(
query(db.select().from(AccountTable).where(eq(AccountTable.id, accountID)).get()).pipe(
Effect.map(Option.fromNullishOr),
),
)
const persistToken = Effect.fn("AccountRepo.persistToken")((input) =>
query((db) =>
query(
db
.update(AccountTable)
.set({
@@ -127,31 +121,36 @@ export const layer: Layer.Layer<Service> = Layer.effect(
)
const persistAccount = Effect.fn("AccountRepo.persistAccount")((input) =>
tx((db) => {
const url = normalizeServerUrl(input.url)
query(
db.transaction((tx) =>
Effect.gen(function* () {
const url = normalizeServerUrl(input.url)
db.insert(AccountTable)
.values({
id: input.id,
email: input.email,
url,
access_token: input.accessToken,
refresh_token: input.refreshToken,
token_expiry: input.expiry,
})
.onConflictDoUpdate({
target: AccountTable.id,
set: {
email: input.email,
url,
access_token: input.accessToken,
refresh_token: input.refreshToken,
token_expiry: input.expiry,
},
})
.run()
void state(db, input.id, input.orgID)
}).pipe(Effect.asVoid),
yield* tx
.insert(AccountTable)
.values({
id: input.id,
email: input.email,
url,
access_token: input.accessToken,
refresh_token: input.refreshToken,
token_expiry: input.expiry,
})
.onConflictDoUpdate({
target: AccountTable.id,
set: {
email: input.email,
url,
access_token: input.accessToken,
refresh_token: input.refreshToken,
token_expiry: input.expiry,
},
})
.run()
yield* state(input.id, input.orgID)
}),
),
).pipe(Effect.asVoid),
)
return Service.of({
@@ -166,4 +165,6 @@ export const layer: Layer.Layer<Service> = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
export * as AccountRepo from "./repo"
+3 -3
View File
@@ -1,9 +1,9 @@
import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk"
import path from "node:path"
import { pathToFileURL } from "node:url"
import type { MessageV2 } from "@/session/message-v2"
import { SessionV1 } from "@opencode-ai/core/v1/session"
export type PromptPart = MessageV2.TextPartInput | MessageV2.FilePartInput
export type PromptPart = SessionV1.TextPartInput | SessionV1.FilePartInput
export type ReplayPart =
| {
@@ -141,7 +141,7 @@ function uriToFilePart(
uri: string,
mime: string,
filename?: string,
): MessageV2.FilePartInput | MessageV2.TextPartInput {
): SessionV1.FilePartInput | SessionV1.TextPartInput {
try {
if (uri.startsWith("file://")) {
return {
+8 -7
View File
@@ -2,15 +2,16 @@ import { Agent } from "@/agent/agent"
import { Command } from "@/command"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceStore } from "@/project/instance-store"
import { ModelID, ProviderID } from "@/provider/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
import { Context, Effect, Layer, SynchronizedRef } from "effect"
import type * as ACPError from "./error"
export type ModelOption = {
readonly providerID: ProviderID
readonly providerID: ProviderV2.ID
readonly providerName: string
readonly modelID: ModelID
readonly modelID: ModelV2.ID
readonly modelName: string
}
@@ -23,13 +24,13 @@ export type ModeOption = {
export type ModelVariants = NonNullable<Provider.Model["variants"]>
export type DefaultModel = {
readonly providerID: ProviderID
readonly modelID: ModelID
readonly providerID: ProviderV2.ID
readonly modelID: ModelV2.ID
}
export type Snapshot = {
readonly directory: string
readonly providers: Record<ProviderID, Provider.Info>
readonly providers: Record<ProviderV2.ID, Provider.Info>
readonly modelOptions: readonly ModelOption[]
readonly variantsByModel: Readonly<Record<string, ModelVariants>>
readonly availableModes: readonly ModeOption[]
@@ -58,7 +59,7 @@ export const variants = (snapshot: Snapshot, model: DefaultModel) => snapshot.va
export const build = (input: {
readonly directory: string
readonly providers: Record<ProviderID, Provider.Info>
readonly providers: Record<ProviderV2.ID, Provider.Info>
readonly modes: readonly ModeOption[]
readonly defaultModeID: string
readonly commands: readonly Command.Info[]
+25
View File
@@ -12,6 +12,7 @@ import type {
import { Effect } from "effect"
import { ACPSession } from "./session"
import { ACPPermission } from "./permission"
import { partsToContentChunks, type ReplayPart } from "./content"
import {
duplicateRunningToolUpdate,
errorToolUpdate,
@@ -87,7 +88,31 @@ export class Subscription {
await this.recordFetchedPart(message.info.sessionID, message, part)
if (part.type === "tool") {
await this.handleToolPart(message.info.sessionID, part)
continue
}
await this.replayContentPart(message, part)
}
}
private async replayContentPart(message: SessionMessageResponse, part: Part) {
if (part.type !== "text" && part.type !== "file" && part.type !== "reasoning") return
const sessionUpdate =
part.type === "reasoning"
? "agent_thought_chunk"
: message.info.role === "user"
? "user_message_chunk"
: "agent_message_chunk"
for (const chunk of partsToContentChunks([part as ReplayPart])) {
await this.input.connection.sessionUpdate({
sessionId: message.info.sessionID,
update: {
sessionUpdate,
messageId: message.info.id,
...chunk,
},
})
}
}
+34 -30
View File
@@ -41,7 +41,8 @@ import { ACPEvent } from "./event"
import { ACPSession } from "./session"
import { UsageService } from "./usage"
import { ACPProfile } from "./profile"
import { ModelID, ProviderID } from "@/provider/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
import type { Command } from "@/command"
@@ -215,11 +216,7 @@ export function make(input: {
"session",
)
const messages = yield* request(
() =>
input.sdk.session.messages(
{ directory: params.cwd, sessionID: params.sessionId, limit: 100 },
{ throwOnError: true },
),
() => input.sdk.session.messages({ directory: params.cwd, sessionID: params.sessionId }, { throwOnError: true }),
"session",
)
const restored = restoreFromMessages(messages.map((item) => item.info))
@@ -332,25 +329,34 @@ export function make(input: {
}
})
const abortBackingSession = Effect.fn("ACP.abortBackingSession")(function* (current: ACPSession.Info) {
yield* request(
() => input.sdk.session.abort({ directory: current.cwd, sessionID: current.id }, { throwOnError: true }),
"session",
).pipe(
Effect.catch((error) =>
Effect.sync(() => {
log.error("failed to abort ACP backing session", { error, sessionID: current.id })
}),
),
)
})
const closeSession = Effect.fn("ACP.closeSession")(function* (params: CloseSessionRequest) {
const removed = yield* session.remove(params.sessionId)
registeredMcp.delete(params.sessionId)
sessionSnapshots.delete(params.sessionId)
if (!removed) return {}
yield* request(
() => input.sdk.session.abort({ directory: removed.cwd, sessionID: params.sessionId }, { throwOnError: true }),
"session",
).pipe(
Effect.catch((error) =>
Effect.sync(() => {
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
}),
),
)
yield* abortBackingSession(removed)
return {}
})
const cancel = Effect.fn("ACP.cancel")(function* (params: CancelNotification) {
const current = yield* session.get(params.sessionId)
yield* abortBackingSession(current)
})
const forkSession = Effect.fn("ACP.forkSession")(function* (params: ForkSessionRequest) {
const snapshot = yield* directorySnapshot(params.cwd)
const forked = yield* request(
@@ -565,9 +571,7 @@ export function make(input: {
yield* sendUsageUpdate(input.usage, input.sdk, input.connection, current.id, current.cwd)
return promptResponse(undefined, params.messageId)
}),
cancel: Effect.fn("ACP.cancel")(function* (_input: CancelNotification) {
return yield* new ACPError.UnsupportedOperationError({ method: "session/cancel" })
}),
cancel,
}
}
@@ -605,7 +609,7 @@ function makeUsageService(sdk: KiloClient) {
.then((response) => {
const providers = Object.fromEntries(
(response.data?.providers ?? []).map((provider) => [provider.id, provider]),
) as Record<ProviderID, Provider.Info>
) as Record<ProviderV2.ID, Provider.Info>
return UsageService.findContextLimit(providers, params.providerID, params.modelID)
})
.catch((error: unknown) => {
@@ -644,8 +648,8 @@ function makeUsageService(sdk: KiloClient) {
const size = yield* contextLimit({
directory: params.directory,
providerID: ProviderID.make(message.providerID),
modelID: ModelID.make(message.modelID),
providerID: ProviderV2.ID.make(message.providerID),
modelID: ModelV2.ID.make(message.modelID),
})
if (!size) return
@@ -747,7 +751,7 @@ async function loadDirectorySnapshot(sdk: KiloClient, directory: string) {
const commandsData = commandsResponse.data!
const skills = skillsResponse.data!
const providers = Object.fromEntries(providersData.providers.map((provider) => [provider.id, provider])) as Record<
ProviderID,
ProviderV2.ID,
Provider.Info
>
const defaultModelStarted = performance.now()
@@ -786,7 +790,7 @@ async function loadDirectorySnapshot(sdk: KiloClient, directory: string) {
function defaultModelFromConfig(
configuredModel: string | undefined,
providers: Record<ProviderID, Provider.Info>,
providers: Record<ProviderV2.ID, Provider.Info>,
): Directory.DefaultModel | undefined {
const configured = configuredModel ? Provider.parseModel(configuredModel) : undefined
if (configured && providers[configured.providerID]?.models[configured.modelID]) return configured
@@ -794,7 +798,7 @@ function defaultModelFromConfig(
// First-session ACP startup must not scan historical sessions just to infer
// a default. Configured model, opencode provider, then sorted best model keep
// the protocol response deterministic without extra session/message reads.
const kiloProvider = providers[ProviderID.kilo] // kilocode_change
const kiloProvider = providers[ProviderV2.ID.make("kilo")] // kilocode_change
const kiloModel = kiloProvider ? Provider.sort(Object.values(kiloProvider.models))[0] : undefined // kilocode_change
if (kiloProvider && kiloModel) return { providerID: kiloProvider.id, modelID: kiloModel.id } // kilocode_change
@@ -807,7 +811,7 @@ function selectDefaultModel(snapshot: Directory.Snapshot) {
if (snapshot.defaultModel) return snapshot.defaultModel
const model = snapshot.modelOptions[0]
if (model) return { providerID: model.providerID, modelID: model.modelID }
return { providerID: "unknown" as ProviderID, modelID: "unknown" as ModelID }
return { providerID: "unknown" as ProviderV2.ID, modelID: "unknown" as ModelV2.ID }
}
function detectSlashCommand(parts: ReturnType<typeof promptContentToParts>) {
@@ -866,8 +870,8 @@ function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
const selected = parseModelSelection(modelId, Object.values(snapshot.providers))
const provider = snapshot.providers[ProviderID.make(selected.model.providerID)]
const model = provider?.models[ModelID.make(selected.model.modelID)]
const provider = snapshot.providers[ProviderV2.ID.make(selected.model.providerID)]
const model = provider?.models[ModelV2.ID.make(selected.model.modelID)]
if (!model) {
return Effect.fail(
new ACPError.InvalidModelError({
@@ -995,7 +999,7 @@ function restoreFromMessages(messages: readonly MessageInfo[]) {
)
if (user?.model?.providerID && user.model.modelID) {
return {
model: { providerID: user.model.providerID as ProviderID, modelID: user.model.modelID as ModelID },
model: { providerID: user.model.providerID as ProviderV2.ID, modelID: user.model.modelID as ModelV2.ID },
variant: user.model.variant,
modeId: user.agent,
}
@@ -1004,7 +1008,7 @@ function restoreFromMessages(messages: readonly MessageInfo[]) {
const assistant = messages.findLast((message) => message.providerID && message.modelID)
if (assistant?.providerID && assistant.modelID) {
return {
model: { providerID: assistant.providerID as ProviderID, modelID: assistant.modelID as ModelID },
model: { providerID: assistant.providerID as ProviderV2.ID, modelID: assistant.modelID as ModelV2.ID },
variant: assistant.variant,
modeId: assistant.mode ?? assistant.agent,
}
+4 -3
View File
@@ -1,12 +1,13 @@
import type { McpServer } from "@agentclientprotocol/sdk"
import type { Message, Part } from "@kilocode/sdk/v2"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Context, Effect, Layer, Ref } from "effect"
import type { ModelID, ProviderID } from "../provider/schema"
import * as ACPError from "./error"
export type SelectedModel = {
providerID: ProviderID
modelID: ModelID
providerID: ProviderV2.ID
modelID: ModelV2.ID
}
export type KnownMessagePartMetadata = {
+35 -8
View File
@@ -46,14 +46,13 @@ export function toToolKind(toolName: string): ToolKind {
return "fetch"
case "edit":
case "apply_patch":
case "patch":
case "write":
return "edit"
case "grep":
case "glob":
case "repo_clone":
case "repo_overview":
case "context":
case "context7_resolve_library_id":
case "context7_get_library_docs":
@@ -62,6 +61,9 @@ export function toToolKind(toolName: string): ToolKind {
case "read":
return "read"
case "task":
return "think"
default:
return "other"
}
@@ -76,10 +78,11 @@ export function toLocations(toolName: string, input: ToolInput): ToolCallLocatio
case "write":
return locationFrom(input.filePath ?? input.filepath)
case "external_directory":
return locationFrom(input.filePath ?? input.filepath, input.parentDir, input.directories)
case "grep":
case "glob":
case "repo_clone":
case "repo_overview":
case "context":
case "context7_resolve_library_id":
case "context7_get_library_docs":
@@ -95,12 +98,14 @@ export function toLocations(toolName: string, input: ToolInput): ToolCallLocatio
}
export function completedToolContent(toolName: string, state: CompletedToolState): ToolCallContent[] {
const text =
toolName.toLocaleLowerCase() === "read" ? (readDisplayText(state.metadata) ?? state.output) : state.output
const content: ToolCallContent[] = [
{
type: "content",
content: {
type: "text",
text: state.output,
text,
},
},
]
@@ -255,9 +260,19 @@ export const buildDuplicateRunningToolUpdate = duplicateRunningToolUpdate
export const buildCompletedToolUpdate = completedToolUpdate
export const buildErrorToolUpdate = errorToolUpdate
function locationFrom(value: unknown): ToolCallLocation[] {
const path = stringValue(value)
return path ? [{ path }] : []
function locationFrom(...values: unknown[]): ToolCallLocation[] {
return Array.from(
new Set(
values.flatMap((value): string[] => {
if (Array.isArray(value)) {
return value.filter((item): item is string => typeof item === "string" && item.length > 0)
}
const path = stringValue(value)
return path ? [path] : []
}),
),
(path) => ({ path }),
)
}
function diffContent(input: ToolInput): ToolCallContent[] {
@@ -275,6 +290,18 @@ function diffContent(input: ToolInput): ToolCallContent[] {
]
}
function readDisplayText(metadata: unknown) {
if (!metadata || typeof metadata !== "object") return undefined
const display = (metadata as Record<string, unknown>).display
if (!display || typeof display !== "object") return undefined
const info = display as Record<string, unknown>
if (info.type === "file") return stringValue(info.text)
if (info.type === "directory" && Array.isArray(info.entries)) {
return info.entries.filter((item): item is string => typeof item === "string").join("\n")
}
return undefined
}
function dataUrlImage(attachment: ToolAttachment) {
const match = stringValue(attachment.url)?.match(/^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/)
const mime = match?.[1] ?? stringValue(attachment.mime)
+14 -13
View File
@@ -3,7 +3,8 @@ import * as Log from "@opencode-ai/core/util/log"
import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@kilocode/sdk/v2"
import { InstanceRef } from "@/effect/instance-ref"
import { InstanceStore } from "@/project/instance-store"
import { ModelID, ProviderID } from "@/provider/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Provider } from "@/provider/provider"
import { Context, Effect, Layer, SynchronizedRef } from "effect"
@@ -38,7 +39,7 @@ export interface MessageLoaderInterface {
}
export interface ContextLimitLoaderInterface {
readonly providers: (directory: string) => Effect.Effect<Record<ProviderID, Provider.Info>, unknown>
readonly providers: (directory: string) => Effect.Effect<Record<ProviderV2.ID, Provider.Info>, unknown>
}
export type UsageConnection = Pick<AgentSideConnection, "sessionUpdate">
@@ -49,8 +50,8 @@ export interface Interface {
readonly totalSessionCost: (messages: readonly SessionMessage[]) => number
readonly contextLimit: (input: {
readonly directory: string
readonly providerID: ProviderID
readonly modelID: ModelID
readonly providerID: ProviderV2.ID
readonly modelID: ModelV2.ID
}) => Effect.Effect<number | undefined>
readonly sendUpdate: (input: {
readonly connection: UsageConnection
@@ -110,9 +111,9 @@ export function totalSessionCost(messages: readonly SessionMessage[]): number {
}
export function findContextLimit(
providers: Record<ProviderID, Provider.Info>,
providerID: ProviderID,
modelID: ModelID,
providers: Record<ProviderV2.ID, Provider.Info>,
providerID: ProviderV2.ID,
modelID: ModelV2.ID,
): number | undefined {
return providers[providerID]?.models[modelID]?.limit.context
}
@@ -143,8 +144,8 @@ export const layer = Layer.effect(
const cachedLimit = Effect.fnUntraced(function* (input: {
readonly directory: string
readonly providerID: ProviderID
readonly modelID: ModelID
readonly providerID: ProviderV2.ID
readonly modelID: ModelV2.ID
}) {
return yield* SynchronizedRef.modifyEffect(
limits,
@@ -170,8 +171,8 @@ export const layer = Layer.effect(
const contextLimit = Effect.fn("ACPUsage.contextLimit")(function* (input: {
readonly directory: string
readonly providerID: ProviderID
readonly modelID: ModelID
readonly providerID: ProviderV2.ID
readonly modelID: ModelV2.ID
}) {
return yield* yield* cachedLimit(input)
})
@@ -197,8 +198,8 @@ export const layer = Layer.effect(
const size = yield* contextLimit({
directory: input.directory,
providerID: ProviderID.make(message.providerID),
modelID: ModelID.make(message.modelID),
providerID: ProviderV2.ID.make(message.providerID),
modelID: ModelV2.ID.make(message.modelID),
})
if (!size) return
+20 -11
View File
@@ -1,7 +1,8 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Config } from "@/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
import { Truncate } from "@/tool/truncate"
import { Auth } from "../auth"
@@ -10,7 +11,7 @@ import { ProviderTransform } from "@/provider/transform"
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
import PROMPT_EXPLORE from "./prompt/explore.txt"
import PROMPT_SCOUT from "./prompt/scout.txt"
import PROMPT_SCOUT from "@/kilocode/agent/scout.txt" // kilocode_change
import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { Permission } from "@/permission"
@@ -22,6 +23,8 @@ import { Plugin } from "@/plugin"
import { Skill } from "../skill"
import { Effect, Context, Layer, Schema } from "effect"
import { InstanceState } from "@/effect/instance-state"
import * as Option from "effect/Option"
import * as OtelTracer from "@effect/opentelemetry/Tracer"
import { type DeepMutable } from "@opencode-ai/core/schema"
import * as KiloAgent from "@/kilocode/agent" // kilocode_change
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -29,6 +32,8 @@ import { Reference } from "@/reference/reference" // kilocode_change
import { ConfigReference } from "@/config/reference" // kilocode_change
import * as AgentRequirements from "@/kilocode/agent-requirements" // kilocode_change
import { MCP } from "@/mcp" // kilocode_change
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
export type RequirementBlockedError = InstanceType<typeof AgentRequirements.BlockedError> // kilocode_change
@@ -44,11 +49,11 @@ export const Info = Schema.Struct({
topP: Schema.optional(Schema.Finite),
temperature: Schema.optional(Schema.Finite),
color: Schema.optional(Schema.String),
permission: Permission.Ruleset,
permission: PermissionV1.Ruleset,
model: Schema.optional(
Schema.Struct({
modelID: ModelID,
providerID: ProviderID,
modelID: ModelV2.ID,
providerID: ProviderV2.ID,
}),
),
variant: Schema.optional(Schema.String),
@@ -74,7 +79,7 @@ export interface Interface {
readonly guardRequirements: (agent: Info) => Effect.Effect<void, RequirementBlockedError> // kilocode_change
readonly generate: (input: {
description: string
model?: { providerID: ProviderID; modelID: ModelID }
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
}) => Effect.Effect<
{
identifier: string
@@ -100,7 +105,7 @@ export const layer = Layer.effect(
const skill = yield* Skill.Service
const mcp = yield* MCP.Service // kilocode_change
const provider = yield* Provider.Service
const flags = yield* RuntimeFlags.Service
const flags = yield* RuntimeFlags.Service // kilocode_change
const state = yield* InstanceState.make<State>(
Effect.fn("Agent.state")(function* (ctx) {
@@ -133,8 +138,8 @@ export const layer = Layer.effect(
interactive_terminal: "deny", // kilocode_change - human-driven tools are primary-agent only
plan_enter: "deny",
plan_exit: "deny",
repo_clone: "deny",
repo_overview: "deny",
repo_clone: "deny", // kilocode_change
repo_overview: "deny", // kilocode_change
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
read: {
"*": "allow",
@@ -229,6 +234,7 @@ export const layer = Layer.effect(
mode: "subagent",
native: true,
},
// kilocode_change start - retain Kilo's opt-in repository research agent
...(flags.experimentalScout
? {
scout: {
@@ -259,6 +265,7 @@ export const layer = Layer.effect(
},
}
: {}),
// kilocode_change end
compaction: {
name: "compaction",
mode: "primary",
@@ -539,7 +546,7 @@ export const layer = Layer.effect(
guardRequirements, // kilocode_change
generate: Effect.fn("Agent.generate")(function* (input: {
description: string
model?: { providerID: ProviderID; modelID: ModelID }
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
}) {
const cfg = yield* config.get()
const model = input.model ?? (yield* provider.defaultModel())
@@ -603,7 +610,9 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(
// kilocode_change start - preserve the concrete layer type across Kilo's Agent/Skill cycle
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
// kilocode_change end
Layer.provide(Plugin.defaultLayer),
Layer.provide(Provider.defaultLayer),
Layer.provide(Auth.defaultLayer),
@@ -1,3 +1,4 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import type { Permission } from "../permission"
import type { Agent } from "./agent"
@@ -15,10 +16,10 @@ import type { Agent } from "./agent"
* doesn't already permit them.
*/
export function deriveSubagentSessionPermission(input: {
parentSessionPermission: Permission.Ruleset
parentSessionPermission: PermissionV1.Ruleset
parentAgent: Agent.Info | undefined
subagent: Agent.Info
}): Permission.Ruleset {
}): PermissionV1.Ruleset {
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
const parentAgentDenies =
+3 -3
View File
@@ -2,7 +2,7 @@ import path from "path"
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Global } from "@opencode-ai/core/global"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
export const OAUTH_DUMMY_KEY = "kilo-oauth-dummy-key" // kilocode_change
@@ -52,7 +52,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Au
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fsys = yield* AppFileSystem.Service
const fsys = yield* FSUtil.Service
const decode = Schema.decodeUnknownOption(Info)
const all = Effect.fn("Auth.all")(function* () {
@@ -99,6 +99,6 @@ export const layer = Layer.effect(
}),
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
export const defaultLayer = layer.pipe(Layer.provide(FSUtil.defaultLayer))
export * as Auth from "."
+25 -189
View File
@@ -1,197 +1,33 @@
import { BackgroundJob as CoreBackgroundJob } from "@opencode-ai/core/background-job"
import { InstanceState } from "@/effect/instance-state"
import { Identifier } from "@/id/id"
import { Cause, Clock, Context, Deferred, Effect, Fiber, Layer, Scope, SynchronizedRef } from "effect"
import { Effect, Layer } from "effect"
export type Status = "running" | "completed" | "error" | "cancelled"
export type Info = {
id: string
type: string
title?: string
status: Status
started_at: number
completed_at?: number
output?: string
error?: string
metadata?: Record<string, unknown>
}
type Active = {
info: Info
done: Deferred.Deferred<Info>
fiber?: Fiber.Fiber<void, unknown>
}
type State = {
jobs: SynchronizedRef.SynchronizedRef<Map<string, Active>>
scope: Scope.Scope
}
type FinishResult = {
info?: Info
done?: Deferred.Deferred<Info>
}
export type StartInput = {
id?: string
type: string
title?: string
metadata?: Record<string, unknown>
run: Effect.Effect<string, unknown>
}
export type WaitInput = {
id: string
timeout?: number
}
export type WaitResult = {
info?: Info
timedOut: boolean
}
export interface Interface {
readonly list: () => Effect.Effect<Info[]>
readonly get: (id: string) => Effect.Effect<Info | undefined>
readonly start: (input: StartInput) => Effect.Effect<Info>
readonly wait: (input: WaitInput) => Effect.Effect<WaitResult>
readonly cancel: (id: string) => Effect.Effect<Info | undefined>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/BackgroundJob") {}
function snapshot(job: Active): Info {
return {
...job.info,
...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}),
}
}
function errorText(error: unknown) {
if (error instanceof Error) return error.message
return String(error)
}
export const layer = Layer.effect(
export {
Service,
type ExtendInput,
type Info,
type Interface,
type StartInput,
type Status,
type WaitInput,
type WaitResult,
} from "@opencode-ai/core/background-job"
/** Keeps the legacy service instance-scoped while sharing the core registry engine. */
export const layer = Layer.effect(
CoreBackgroundJob.Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(
Effect.fn("BackgroundJob.state")(function* () {
return {
jobs: yield* SynchronizedRef.make(new Map()),
scope: yield* Scope.Scope,
}
}),
)
const finish = Effect.fn("BackgroundJob.finish")(function* (
id: string,
status: Exclude<Status, "running">,
data?: { output?: string; error?: string },
) {
const completed_at = yield* Clock.currentTimeMillis
const result = yield* SynchronizedRef.modify(
(yield* InstanceState.get(state)).jobs,
(jobs): readonly [FinishResult, Map<string, Active>] => {
const job = jobs.get(id)
if (!job) return [{}, jobs]
if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs]
const next = {
...job,
fiber: undefined,
info: {
...job.info,
status,
completed_at,
...(data?.output !== undefined ? { output: data.output } : {}),
...(data?.error !== undefined ? { error: data.error } : {}),
},
}
return [{ info: snapshot(next), done: job.done }, new Map(jobs).set(id, next)]
},
)
if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore)
return result.info
const state = yield* InstanceState.make(() => CoreBackgroundJob.make)
return CoreBackgroundJob.Service.of({
list: () => InstanceState.useEffect(state, (jobs) => jobs.list()),
get: (id) => InstanceState.useEffect(state, (jobs) => jobs.get(id)),
start: (input) => InstanceState.useEffect(state, (jobs) => jobs.start(input)),
extend: (input) => InstanceState.useEffect(state, (jobs) => jobs.extend(input)),
wait: (input) => InstanceState.useEffect(state, (jobs) => jobs.wait(input)),
waitForPromotion: (id) => InstanceState.useEffect(state, (jobs) => jobs.waitForPromotion(id)),
promote: (id) => InstanceState.useEffect(state, (jobs) => jobs.promote(id)),
cancel: (id) => InstanceState.useEffect(state, (jobs) => jobs.cancel(id)),
})
const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () {
return Array.from((yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).values())
.map(snapshot)
.toSorted((a, b) => a.started_at - b.started_at)
})
const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) {
const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(id)
if (!job) return
return snapshot(job)
})
const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) {
return yield* Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const s = yield* InstanceState.get(state)
const id = input.id ?? Identifier.ascending("job")
const started_at = yield* Clock.currentTimeMillis
const done = yield* Deferred.make<Info>()
return yield* SynchronizedRef.modifyEffect(
s.jobs,
Effect.fnUntraced(function* (jobs) {
const existing = jobs.get(id)
if (existing?.info.status === "running") return [snapshot(existing), jobs] as const
const fiber = yield* restore(input.run).pipe(
Effect.matchCauseEffect({
onSuccess: (output) => finish(id, "completed", { output }),
onFailure: (cause) =>
finish(id, Cause.hasInterruptsOnly(cause) ? "cancelled" : "error", {
error: errorText(Cause.squash(cause)),
}),
}),
Effect.asVoid,
Effect.forkIn(s.scope, { startImmediately: true }),
)
const job = {
info: {
id,
type: input.type,
title: input.title,
status: "running" as const,
started_at,
metadata: input.metadata,
},
done,
fiber,
}
return [snapshot(job), new Map(jobs).set(id, job)] as const
}),
)
}),
)
})
const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) {
const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(input.id)
if (!job) return { timedOut: false }
if (job.info.status !== "running") return { info: snapshot(job), timedOut: false }
if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false }
if (input.timeout <= 0) return { info: snapshot(job), timedOut: true }
const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout))
if (info._tag === "Some") return { info: info.value, timedOut: false }
return { info: snapshot(job), timedOut: true }
})
const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) {
const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(id)
if (!job) return
if (job.info.status !== "running") return snapshot(job)
if (job.fiber) {
yield* Fiber.interrupt(job.fiber).pipe(Effect.ignore)
yield* Fiber.await(job.fiber).pipe(Effect.ignore)
}
const info = yield* finish(id, "cancelled")
return info
})
return Service.of({ list, get, start, wait, cancel })
}),
)
+4
View File
@@ -1,3 +1,7 @@
// kilocode_change - Kilo compatibility layer. Upstream deleted this Bus (Effect PubSub) service in v1.16.2
// in favour of EventV2; Kilo keeps it ONLY for existing Kilo-owned callers (kilocode/* features) that rely on
// its eager-callback subscription + fork-atomicity semantics. Do NOT add new shared/upstream-shaped consumers.
// Full migration of Kilo callers onto core EventV2 is tracked as a dedicated follow-up.
import { Effect, Exit, Fiber, Layer, PubSub, Scope, Context, Stream, Schema } from "effect" // kilocode_change
import { EffectBridge } from "@/effect/bridge"
import * as Log from "@opencode-ai/core/util/log"
+2 -2
View File
@@ -2,8 +2,6 @@ import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
import { ACP } from "@/acp/agent"
import { Server } from "@/server/server"
import { ServerAuth } from "@/server/auth"
import { createKiloClient } from "@kilocode/sdk/v2"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
@@ -22,6 +20,8 @@ export const AcpCommand = effectCmd({
})
},
handler: Effect.fn("Cli.acp")(function* (args) {
const { Server } = yield* Effect.promise(() => import("@/server/server"))
const { ACP } = yield* Effect.promise(() => import("@/acp/agent"))
ACPProfile.mark("cli.acp.handler")
process.env.KILO_CLIENT = "acp"
const opts = yield* resolveNetworkOptions(args)
+4 -3
View File
@@ -2,13 +2,10 @@ import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { Global } from "@opencode-ai/core/global"
import { Agent } from "../../agent/agent"
import { Provider } from "@/provider/provider"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "@/util/filesystem"
import matter from "gray-matter"
import { InstanceRef } from "@/effect/instance-ref"
import { EOL } from "os"
import type { Argv } from "yargs"
import { Effect } from "effect"
@@ -62,6 +59,9 @@ const AgentCreateCommand = effectCmd({
describe: "model to use in the format of provider/model",
}),
handler: Effect.fn("Cli.agent.create")(function* (args) {
const { InstanceRef } = yield* Effect.promise(() => import("@/effect/instance-ref"))
const { Agent } = yield* Effect.promise(() => import("../../agent/agent"))
const { Provider } = yield* Effect.promise(() => import("@/provider/provider"))
const maybeCtx = yield* InstanceRef
if (!maybeCtx) return yield* Effect.die("InstanceRef not provided")
const ctx = maybeCtx
@@ -238,6 +238,7 @@ const AgentListCommand = effectCmd({
command: "list",
describe: "list all available agents",
handler: Effect.fn("Cli.agent.list")(function* () {
const { Agent } = yield* Effect.promise(() => import("../../agent/agent"))
const agents = yield* Agent.Service.use((svc) => svc.list())
const sortedAgents = agents.sort((a, b) => {
if (a.native !== b.native) {
+26 -84
View File
@@ -1,17 +1,14 @@
import type { Argv } from "yargs"
import { spawn } from "child_process"
import { Database } from "@/storage/db"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { Database as BunDatabase } from "bun:sqlite"
import { UI } from "../ui"
import { cmd } from "./cmd"
import { JsonMigration } from "@/storage/json-migration"
import { EOL } from "os"
import { errorMessage } from "../../util/error"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { sql } from "drizzle-orm"
import { effectCmd } from "../effect-cmd"
const QueryCommand = cmd({
const QueryCommand = effectCmd({
command: "$0 [query]",
describe: "open an interactive sqlite3 shell or run a query",
instance: false,
builder: (yargs: Argv) => {
return yargs
.positional("query", {
@@ -25,97 +22,42 @@ const QueryCommand = cmd({
describe: "Output format",
})
},
handler: async (args: { query?: string; format: string }) => {
handler: Effect.fn("Cli.db.query")(function* (args: { query?: string; format: string }) {
const query = args.query as string | undefined
if (query) {
const db = new BunDatabase(Database.getPath(), { readonly: true })
try {
const result = db.query(query).all() as Record<string, unknown>[]
if (args.format === "json") {
console.log(JSON.stringify(result, null, 2))
} else if (result.length > 0) {
const keys = Object.keys(result[0])
console.log(keys.join("\t"))
for (const row of result) {
console.log(keys.map((k) => row[k]).join("\t"))
}
}
} catch (err) {
UI.error(errorMessage(err))
process.exit(1)
const { db } = yield* Database.Service
const result = yield* db.all<Record<string, unknown>>(sql.raw(query)).pipe(Effect.orDie)
if (args.format === "json") console.log(JSON.stringify(result, null, 2))
else if (result.length > 0) {
const keys = Object.keys(result[0])
console.log(keys.join("\t"))
for (const row of result) console.log(keys.map((key) => row[key]).join("\t"))
}
db.close()
return
}
const child = spawn("sqlite3", [Database.getPath()], {
const child = spawn("sqlite3", [Database.path()], {
stdio: "inherit",
windowsHide: true, // kilocode_change - prevent CMD window flash on Windows
})
await new Promise((resolve) => child.on("close", resolve))
},
yield* Effect.promise(() => new Promise((resolve) => child.on("close", resolve)))
}),
})
const PathCommand = cmd({
const PathCommand = effectCmd({
command: "path",
describe: "print the database path",
handler: () => {
console.log(Database.getPath())
},
instance: false,
handler: Effect.fn("Cli.db.path")(function* () {
console.log(Database.path())
}),
})
const MigrateCommand = cmd({
command: "migrate",
describe: "migrate JSON data to SQLite (merges with existing data)",
handler: async () => {
const sqlite = new BunDatabase(Database.getPath())
const tty = process.stderr.isTTY
const width = 36
const orange = "\x1b[38;5;214m"
const muted = "\x1b[0;2m"
const reset = "\x1b[0m"
let last = -1
if (tty) process.stderr.write("\x1b[?25l")
try {
const stats = await JsonMigration.run(drizzle({ client: sqlite }), {
progress: (event) => {
const percent = Math.floor((event.current / event.total) * 100)
if (percent === last) return
last = percent
if (tty) {
const fill = Math.round((percent / 100) * width)
const bar = `${"■".repeat(fill)}${"・".repeat(width - fill)}`
process.stderr.write(
`\r${orange}${bar} ${percent.toString().padStart(3)}%${reset} ${muted}${event.current}/${event.total}${reset} `,
)
} else {
process.stderr.write(`sqlite-migration:${percent}${EOL}`)
}
},
})
if (tty) process.stderr.write("\n")
if (tty) process.stderr.write("\x1b[?25h")
else process.stderr.write(`sqlite-migration:done${EOL}`)
UI.println(
`Migration complete: ${stats.projects} projects, ${stats.sessions} sessions, ${stats.messages} messages`,
)
if (stats.errors.length > 0) {
UI.println(`${stats.errors.length} errors occurred during migration`)
}
} catch (err) {
if (tty) process.stderr.write("\x1b[?25h")
UI.error(`Migration failed: ${errorMessage(err)}`)
process.exit(1)
} finally {
sqlite.close()
}
},
})
export const DbCommand = cmd({
export const DbCommand = effectCmd({
command: "db",
describe: "database tools",
instance: false,
builder: (yargs: Argv) => {
return yargs.command(QueryCommand).command(PathCommand).command(MigrateCommand).demandCommand()
return yargs.command(QueryCommand).command(PathCommand).demandCommand()
},
handler: () => {},
handler: Effect.fn("Cli.db")(function* () {}),
})
@@ -0,0 +1,193 @@
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { EOL } from "os"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { basename } from "path"
import { Cause, Effect } from "effect"
import { Agent } from "../../../agent/agent"
import { Provider } from "@/provider/provider"
import { Session } from "@/session/session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
import { ToolRegistry } from "@/tool/registry"
import { Permission } from "../../../permission"
import { iife } from "../../../util/iife"
import { fail } from "../../effect-cmd"
import { InstanceRef } from "@/effect/instance-ref"
import type { InstanceContext } from "@/project/instance-context"
export const debugAgent = Effect.fn("Cli.debug.agent")(function* (args: {
name: string
tool?: string
params?: string
}) {
const ctx = yield* InstanceRef
if (!ctx) return
return yield* run(args, ctx)
})
const run = Effect.fn("Cli.debug.agent.body")(function* (
args: { name: string; tool?: string; params?: string },
ctx: InstanceContext,
) {
const agentName = args.name
const agent = yield* Agent.Service.use((svc) => svc.get(agentName))
if (!agent) {
process.stderr.write(
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
)
return yield* fail("", 1)
}
const availableTools = yield* getAvailableTools(agent)
const resolvedTools = resolveTools(agent, availableTools)
const toolID = args.tool
if (toolID) {
const tool = availableTools.find((item) => item.id === toolID)
if (!tool) {
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
return yield* fail("", 1)
}
if (resolvedTools[toolID] === false) {
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
return yield* fail("", 1)
}
const params = parseToolParams(args.params)
const toolCtx = yield* createToolContext(agent, ctx)
const result = yield* tool.execute(params, toolCtx)
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
return
}
const output = {
...agent,
tools: resolvedTools,
}
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
})
const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) {
const provider = yield* Provider.Service
const registry = yield* ToolRegistry.Service
const model =
agent.model ??
(yield* provider.defaultModel().pipe(
Effect.matchCauseEffect({
onSuccess: Effect.succeed,
onFailure: (cause) => {
const error = Cause.squash(cause) as Provider.DefaultModelError
if (error instanceof Provider.ModelNotFoundError) {
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
}
if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`)
return fail("No providers found")
},
}),
))
return yield* registry.tools({ ...model, agent })
})
function resolveTools(agent: Agent.Info, availableTools: { id: string }[]) {
const disabled = Permission.disabled(
availableTools.map((tool) => tool.id),
agent.permission,
)
const resolved: Record<string, boolean> = {}
for (const tool of availableTools) {
resolved[tool.id] = !disabled.has(tool.id)
}
return resolved
}
function parseToolParams(input?: string) {
if (!input) return {}
const trimmed = input.trim()
if (trimmed.length === 0) return {}
const parsed = iife(() => {
try {
return JSON.parse(trimmed)
} catch (jsonError) {
try {
return new Function(`return (${trimmed})`)()
} catch (evalError) {
throw new Error(
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
{ cause: evalError },
)
}
}
})
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Tool params must be an object.")
}
return parsed as Record<string, unknown>
}
const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(function* (
agent: Agent.Info,
ctx: InstanceContext,
) {
const sessionSvc = yield* Session.Service
const session = yield* sessionSvc.create({ title: `Debug tool run (${agent.name})` })
const messageID = MessageID.ascending()
const model = agent.model
? agent.model
: yield* Effect.gen(function* () {
const provider = yield* Provider.Service
return yield* provider.defaultModel().pipe(
Effect.matchCauseEffect({
onSuccess: Effect.succeed,
onFailure: (cause) => {
const error = Cause.squash(cause) as Provider.DefaultModelError
if (error instanceof Provider.ModelNotFoundError) {
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
}
if (error instanceof Provider.NoModelsError)
return fail(`No models found for provider ${error.providerID}`)
return fail("No providers found")
},
}),
)
})
const now = Date.now()
const message: SessionV1.Assistant = {
id: messageID,
sessionID: session.id,
role: "assistant",
time: { created: now },
parentID: messageID,
modelID: model.modelID,
providerID: model.providerID,
mode: "debug",
agent: agent.name,
path: {
cwd: ctx.directory,
root: ctx.worktree,
},
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
}
yield* sessionSvc.updateMessage(message)
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
return {
sessionID: session.id,
messageID,
callID: PartID.ascending(),
agent: agent.name,
abort: new AbortController().signal,
messages: [],
metadata: () => Effect.void,
ask(req: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">) {
return Effect.sync(() => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new PermissionV1.DeniedError({ ruleset })
}
}
})
},
}
})
+7 -186
View File
@@ -1,17 +1,5 @@
import { EOL } from "os"
import { basename } from "path"
import { Cause, Effect } from "effect"
import { Agent } from "../../../agent/agent"
import { Provider } from "@/provider/provider"
import { Session } from "@/session/session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
import { ToolRegistry } from "@/tool/registry"
import { Permission } from "../../../permission"
import { iife } from "../../../util/iife"
import { effectCmd, fail } from "../../effect-cmd"
import { InstanceRef } from "@/effect/instance-ref"
import type { InstanceContext } from "@/project/instance-context"
import { Effect } from "effect"
import { effectCmd } from "../../effect-cmd"
export const AgentCommand = effectCmd({
command: "agent <name>",
@@ -31,176 +19,9 @@ export const AgentCommand = effectCmd({
type: "string",
description: "Tool params as JSON or a JS object literal",
}),
handler: Effect.fn("Cli.debug.agent")(function* (args) {
const ctx = yield* InstanceRef
if (!ctx) return
return yield* run(args, ctx)
}),
})
const run = Effect.fn("Cli.debug.agent.body")(function* (
args: { name: string; tool?: string; params?: string },
ctx: InstanceContext,
) {
const agentName = args.name
const agent = yield* Agent.Service.use((svc) => svc.get(agentName))
if (!agent) {
process.stderr.write(
`Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
)
return yield* fail("", 1)
}
const availableTools = yield* getAvailableTools(agent)
const resolvedTools = resolveTools(agent, availableTools)
const toolID = args.tool
if (toolID) {
const tool = availableTools.find((item) => item.id === toolID)
if (!tool) {
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
return yield* fail("", 1)
}
if (resolvedTools[toolID] === false) {
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
return yield* fail("", 1)
}
const params = parseToolParams(args.params)
const toolCtx = yield* createToolContext(agent, ctx)
const result = yield* tool.execute(params, toolCtx)
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
return
}
const output = {
...agent,
tools: resolvedTools,
}
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
})
const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) {
const provider = yield* Provider.Service
const registry = yield* ToolRegistry.Service
const model =
agent.model ??
(yield* provider.defaultModel().pipe(
Effect.matchCauseEffect({
onSuccess: Effect.succeed,
onFailure: (cause) => {
const error = Cause.squash(cause) as Provider.DefaultModelError
if (error instanceof Provider.ModelNotFoundError) {
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
}
if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`)
return fail("No providers found")
},
}),
))
return yield* registry.tools({ ...model, agent })
})
function resolveTools(agent: Agent.Info, availableTools: { id: string }[]) {
const disabled = Permission.disabled(
availableTools.map((tool) => tool.id),
agent.permission,
)
const resolved: Record<string, boolean> = {}
for (const tool of availableTools) {
resolved[tool.id] = !disabled.has(tool.id)
}
return resolved
}
function parseToolParams(input?: string) {
if (!input) return {}
const trimmed = input.trim()
if (trimmed.length === 0) return {}
const parsed = iife(() => {
try {
return JSON.parse(trimmed)
} catch (jsonError) {
try {
return new Function(`return (${trimmed})`)()
} catch (evalError) {
throw new Error(
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
{ cause: evalError },
)
}
}
})
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Tool params must be an object.")
}
return parsed as Record<string, unknown>
}
const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(function* (
agent: Agent.Info,
ctx: InstanceContext,
) {
const sessionSvc = yield* Session.Service
const session = yield* sessionSvc.create({ title: `Debug tool run (${agent.name})` })
const messageID = MessageID.ascending()
const model = agent.model
? agent.model
: yield* Effect.gen(function* () {
const provider = yield* Provider.Service
return yield* provider.defaultModel().pipe(
Effect.matchCauseEffect({
onSuccess: Effect.succeed,
onFailure: (cause) => {
const error = Cause.squash(cause) as Provider.DefaultModelError
if (error instanceof Provider.ModelNotFoundError) {
return fail(`Model not found: ${error.providerID}/${error.modelID}`)
}
if (error instanceof Provider.NoModelsError)
return fail(`No models found for provider ${error.providerID}`)
return fail("No providers found")
},
}),
)
})
const now = Date.now()
const message: MessageV2.Assistant = {
id: messageID,
sessionID: session.id,
role: "assistant",
time: { created: now },
parentID: messageID,
modelID: model.modelID,
providerID: model.providerID,
mode: "debug",
agent: agent.name,
path: {
cwd: ctx.directory,
root: ctx.worktree,
},
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
}
yield* sessionSvc.updateMessage(message)
const ruleset = Permission.merge(agent.permission, session.permission ?? [])
return {
sessionID: session.id,
messageID,
callID: PartID.ascending(),
agent: agent.name,
abort: new AbortController().signal,
messages: [],
metadata: () => Effect.void,
ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
return Effect.sync(() => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset })
}
}
})
},
}
handler: (args) =>
Effect.gen(function* () {
const { debugAgent } = yield* Effect.promise(() => import("./agent.handler"))
return yield* debugAgent(args)
}),
})
@@ -1,6 +1,5 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Config } from "@/config/config"
import { effectCmd } from "../../effect-cmd"
export const ConfigCommand = effectCmd({
@@ -8,6 +7,7 @@ export const ConfigCommand = effectCmd({
describe: "show resolved configuration",
builder: (yargs) => yargs,
handler: Effect.fn("Cli.debug.config")(function* () {
const { Config } = yield* Effect.promise(() => import("@/config/config"))
const config = yield* Config.Service.use((cfg) => cfg.get())
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
}),
+14 -17
View File
@@ -1,10 +1,18 @@
import { EOL } from "os"
import { Effect } from "effect"
import { File } from "../../../file"
import { Ripgrep } from "@/file/ripgrep"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
const filesystem = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(
Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })),
Effect.provide(LocationServiceMap.layer),
)
const FileSearchCommand = effectCmd({
command: "search <query>",
describe: "search files by query",
@@ -15,8 +23,8 @@ const FileSearchCommand = effectCmd({
description: "Search query",
}),
handler: Effect.fn("Cli.debug.file.search")(function* (args) {
const results = yield* File.Service.use((svc) => svc.search({ query: args.query }))
process.stdout.write(results.join(EOL) + EOL)
const results = yield* filesystem(FileSystem.Service.use((svc) => svc.find({ query: args.query })))
process.stdout.write(results.map((item) => item.path).join(EOL) + EOL)
}),
})
@@ -30,21 +38,11 @@ const FileReadCommand = effectCmd({
description: "File path to read",
}),
handler: Effect.fn("Cli.debug.file.read")(function* (args) {
const content = yield* File.Service.use((svc) => svc.read(args.path))
const content = yield* filesystem(FileSystem.Service.use((svc) => svc.read({ path: RelativePath.make(args.path) })))
process.stdout.write(JSON.stringify(content, null, 2) + EOL)
}),
})
const FileStatusCommand = effectCmd({
command: "status",
describe: "show file status information",
builder: (yargs) => yargs,
handler: Effect.fn("Cli.debug.file.status")(function* () {
const status = yield* File.Service.use((svc) => svc.status())
process.stdout.write(JSON.stringify(status, null, 2) + EOL)
}),
})
const FileListCommand = effectCmd({
command: "list <path>",
describe: "list files in a directory",
@@ -55,7 +53,7 @@ const FileListCommand = effectCmd({
description: "File path to list",
}),
handler: Effect.fn("Cli.debug.file.list")(function* (args) {
const files = yield* File.Service.use((svc) => svc.list(args.path))
const files = yield* filesystem(FileSystem.Service.use((svc) => svc.list({ path: RelativePath.make(args.path) })))
process.stdout.write(JSON.stringify(files, null, 2) + EOL)
}),
})
@@ -81,7 +79,6 @@ export const FileCommand = cmd({
builder: (yargs) =>
yargs
.command(FileReadCommand)
.command(FileStatusCommand)
.command(FileListCommand)
.command(FileSearchCommand)
.command(FileTreeCommand)
+2 -2
View File
@@ -3,8 +3,6 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { Flag } from "@opencode-ai/core/flag/flag"
import os from "os"
import { Duration, Effect } from "effect"
import { Config } from "@/config/config"
import { ConfigPlugin } from "@/config/plugin"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
import { ConfigCommand } from "./config"
@@ -52,6 +50,8 @@ const InfoCommand = effectCmd({
command: "info",
describe: "show debug information",
handler: Effect.fn("Cli.debug.info")(function* () {
const { Config } = yield* Effect.promise(() => import("@/config/config"))
const { ConfigPlugin } = yield* Effect.promise(() => import("@/config/plugin"))
const config = yield* Config.Service.use((cfg) => cfg.get())
const termProgram = process.env.TERM_PROGRAM
? `${process.env.TERM_PROGRAM}${process.env.TERM_PROGRAM_VERSION ? ` ${process.env.TERM_PROGRAM_VERSION}` : ""}`
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { Effect, Stream } from "effect"
import { Ripgrep } from "../../../file/ripgrep"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
import { InstanceRef } from "@/effect/instance-ref"
+4 -2
View File
@@ -1,5 +1,4 @@
import { EOL } from "os"
import { Project } from "@/project/project"
import * as Log from "@opencode-ai/core/util/log"
import { cmd } from "../cmd"
@@ -8,8 +7,11 @@ export const ScrapCommand = cmd({
describe: "list all known projects",
builder: (yargs) => yargs,
async handler() {
const { Project } = await import("@/project/project")
const { makeRuntime } = await import("@opencode-ai/core/effect/runtime")
const runtime = makeRuntime(Project.Service, Project.defaultLayer)
const timer = Log.Default.time("scrap")
const list = await Project.list()
const list = await runtime.runPromise((project) => project.list())
process.stdout.write(JSON.stringify(list, null, 2) + EOL)
timer.stop()
},
+5 -4
View File
@@ -1,4 +1,5 @@
import { Session } from "@/session/session"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema"
import { effectCmd, fail } from "../effect-cmd"
@@ -36,7 +37,7 @@ function diff(
}))
}
function source(part: MessageV2.FilePart) {
function source(part: SessionV1.FilePart) {
if (!part.source) return part.source
if (part.source.type === "symbol") {
return {
@@ -61,7 +62,7 @@ function source(part: MessageV2.FilePart) {
}
}
function filepart(part: MessageV2.FilePart): MessageV2.FilePart {
function filepart(part: SessionV1.FilePart): SessionV1.FilePart {
return {
...part,
url: redact("file-url", part.id, part.url),
@@ -70,7 +71,7 @@ function filepart(part: MessageV2.FilePart): MessageV2.FilePart {
}
}
function part(part: MessageV2.Part): MessageV2.Part {
function part(part: SessionV1.Part): SessionV1.Part {
switch (part.type) {
case "text":
return {
@@ -164,7 +165,7 @@ function part(part: MessageV2.Part): MessageV2.Part {
const partFn = part
function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] }) {
function sanitize(data: { info: Session.Info; messages: SessionV1.WithParts[] }) {
return {
info: {
...data.info,
+1 -1
View File
@@ -1,4 +1,3 @@
import { Server } from "../../server/server"
import type { CommandModule } from "yargs"
type Args = {}
@@ -7,6 +6,7 @@ export const GenerateCommand = {
command: "generate",
builder: (yargs) => yargs,
handler: async () => {
const { Server } = await import("../../server/server")
const specs = (await Server.openapi()) as {
info: { title: string; description: string } // kilocode_change
paths: Record<string, Record<string, any>>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
import type { SessionV1 } from "@opencode-ai/core/v1/session"
export { parseGitHubRemote } from "@/util/repository"
/**
* Extracts displayable text from assistant response parts.
* Returns null for non-text responses (signals summary needed).
* Throws only for truly empty responses.
*/
export function extractResponseText(parts: SessionV1.Part[]): string | null {
const textPart = parts.findLast((p) => p.type === "text")
if (textPart) return textPart.text
// Non-text parts (tools, reasoning, step-start/step-finish, etc.) - signal summary needed
if (parts.length > 0) return null
throw new Error("Failed to parse response: no parts returned")
}
/**
* Formats a PROMPT_TOO_LARGE error message with details about files in the prompt.
* Content is base64 encoded, so we calculate original size by multiplying by 0.75.
*/
export function formatPromptTooLargeError(files: { filename: string; content: string }[]): string {
const fileDetails =
files.length > 0
? `\n\nFiles in prompt:\n${files.map((f) => ` - ${f.filename} (${((f.content.length * 0.75) / 1024).toFixed(0)} KB)`).join("\n")}`
: ""
return `PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.${fileDetails}`
}
File diff suppressed because it is too large Load Diff
+41 -43
View File
@@ -1,21 +1,21 @@
import type { Session as SDKSession, Message, Part } from "@kilocode/sdk/v2"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2"
import { CliError, effectCmd } from "../effect-cmd"
import { Database } from "@/storage/db"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { Database } from "@opencode-ai/core/database/database"
import { SessionTable, MessageTable, PartTable } from "@opencode-ai/core/session/sql"
import { InstanceRef } from "@/effect/instance-ref"
import { EOL } from "os"
import path from "path"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect, Schema } from "effect"
import * as Log from "@opencode-ai/core/util/log" // kilocode_change
import type { InstanceContext } from "@/project/instance-context"
const log = Log.create({ service: "import" }) // kilocode_change
const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info)
const decodePart = Schema.decodeUnknownSync(MessageV2.Part)
const decodeMessageInfo = Schema.decodeUnknownSync(SessionV1.Info)
const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData =
@@ -138,7 +138,8 @@ export const ImportCommand = effectCmd({
})
const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: InstanceContext) {
const fs = yield* AppFileSystem.Service
const fs = yield* FSUtil.Service
const { db } = yield* Database.Service
let exportData: ExportData | undefined
@@ -205,48 +206,45 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
path: path.relative(path.resolve(ctx.worktree), ctx.directory).replaceAll("\\", "/"),
}) as Session.Info
const row = Session.toRow(info)
Database.use((db) =>
db
.insert(SessionTable)
.values(row)
.onConflictDoUpdate({
target: SessionTable.id,
set: { project_id: row.project_id, directory: row.directory, path: row.path },
})
.run(),
)
yield* db
.insert(SessionTable)
.values(row)
.onConflictDoUpdate({
target: SessionTable.id,
set: { project_id: row.project_id, directory: row.directory, path: row.path },
})
.run()
.pipe(Effect.orDie)
for (const msg of exportData.messages) {
const msgInfo = decodeMessageInfo(msg.info) as MessageV2.Info
const msgInfo = decodeMessageInfo(msg.info) as SessionV1.Info
const { id, sessionID: _, ...msgData } = msgInfo
Database.use((db) =>
db
.insert(MessageTable)
.values({
id,
session_id: row.id,
time_created: msgInfo.time?.created ?? Date.now(),
data: msgData,
})
.onConflictDoNothing()
.run(),
)
yield* db
.insert(MessageTable)
.values({
id,
session_id: row.id,
time_created: msgInfo.time?.created ?? Date.now(),
data: msgData as never,
})
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
for (const part of msg.parts) {
const partInfo = decodePart(part) as MessageV2.Part
const partInfo = decodePart(part) as SessionV1.Part
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
Database.use((db) =>
db
.insert(PartTable)
.values({
id: partId,
message_id: messageID,
session_id: row.id,
data: partData,
})
.onConflictDoNothing()
.run(),
)
yield* db
.insert(PartTable)
.values({
id: partId,
message_id: messageID,
session_id: row.id,
data: partData,
})
.onConflictDoNothing()
.run()
.pipe(Effect.orDie)
}
}
+19 -13
View File
@@ -1,4 +1,5 @@
import { cmd } from "./cmd"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { effectCmd } from "../effect-cmd"
import { Cause } from "effect"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
@@ -10,7 +11,7 @@ import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "@/config/config"
import { ConfigMCP } from "../../config/mcp"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import { InstanceRef } from "@/effect/instance-ref"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path"
@@ -18,7 +19,8 @@ import { Global } from "@opencode-ai/core/global"
import { modify, applyEdits } from "jsonc-parser"
import { KilocodeMcpConfig } from "@/kilocode/cli/cmd/mcp" // kilocode_change
import { Filesystem } from "@/util/filesystem"
import { Bus } from "../../bus"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import { Effect } from "effect"
function getAuthStatusIcon(status: MCP.AuthStatus): string {
@@ -43,9 +45,9 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
}
}
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
type McpEntry = NonNullable<ConfigV1.Info["mcp"]>[string]
type McpConfigured = ConfigMCP.Info
type McpConfigured = ConfigMCPV1.Info
function isMcpConfigured(config: McpEntry): config is McpConfigured {
return typeof config === "object" && config !== null && "type" in config
}
@@ -55,11 +57,11 @@ function isMcpRemote(config: McpEntry): config is McpRemote {
return isMcpConfigured(config) && config.type === "remote"
}
function configuredServers(config: Config.Info) {
function configuredServers(config: ConfigV1.Info) {
return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1]))
}
function oauthServers(config: Config.Info) {
function oauthServers(config: ConfigV1.Info) {
return configuredServers(config).filter(
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
)
@@ -257,13 +259,17 @@ export const McpAuthCommand = effectCmd({
spinner.start("Starting OAuth flow...")
// Subscribe to browser open failure events to show URL for manual opening
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
if (evt.properties.mcpName === serverName) {
const events = yield* EventV2Bridge.Service
const unsubscribe = yield* events.listen((event) => {
if (event.type !== MCP.BrowserOpenFailed.type) return Effect.void
const data = event.data as EventV2.Data<typeof MCP.BrowserOpenFailed>
if (data.mcpName === serverName) {
spinner.stop("Could not open browser automatically")
prompts.log.warn("Please open this URL in your browser to authenticate:")
prompts.log.info(evt.properties.url)
prompts.log.info(data.url)
spinner.start("Waiting for authorization...")
}
return Effect.void
})
yield* MCP.Service.use((mcp) => mcp.authenticate(serverName)).pipe(
@@ -301,7 +307,7 @@ export const McpAuthCommand = effectCmd({
prompts.log.error(error instanceof Error ? error.message : String(error))
}),
),
Effect.ensuring(Effect.sync(() => unsubscribe())),
Effect.ensuring(unsubscribe),
)
prompts.outro("Done")
@@ -429,7 +435,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
// kilocode_change end
}
async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPath: string) {
async function addMcpToConfig(name: string, mcpConfig: ConfigMCPV1.Info, configPath: string) {
let text = "{}"
if (await Filesystem.exists(configPath)) {
text = await Filesystem.readText(configPath)
@@ -518,7 +524,7 @@ export const McpAddCommand = effectCmd({
})
if (prompts.isCancel(command)) throw new UI.CancelledError()
const mcpConfig: ConfigMCP.Info = {
const mcpConfig: ConfigMCPV1.Info = {
type: "local",
command: command.split(" "),
}
@@ -548,7 +554,7 @@ export const McpAddCommand = effectCmd({
})
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
let mcpConfig: ConfigMCP.Info
let mcpConfig: ConfigMCPV1.Info
if (useOAuth) {
const hasClientId = await prompts.confirm({
+5 -5
View File
@@ -1,10 +1,9 @@
import { EOL } from "os"
import { Effect } from "effect"
import { Provider } from "@/provider/provider"
import { ProviderID } from "../../provider/schema"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { effectCmd, fail } from "../effect-cmd"
import { UI } from "../ui"
import { ProviderV2 } from "@opencode-ai/core/provider"
export const ModelsCommand = effectCmd({
command: "models [provider]",
@@ -25,6 +24,7 @@ export const ModelsCommand = effectCmd({
type: "boolean",
}),
handler: Effect.fn("Cli.models")(function* (args) {
const { Provider } = yield* Effect.promise(() => import("@/provider/provider"))
if (args.refresh) {
yield* ModelsDev.Service.use((s) => s.refresh(true))
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "Models cache refreshed" + UI.Style.TEXT_NORMAL)
@@ -33,7 +33,7 @@ export const ModelsCommand = effectCmd({
const provider = yield* Provider.Service
const providers = yield* provider.list()
const print = (providerID: ProviderID, verbose?: boolean) => {
const print = (providerID: ProviderV2.ID, verbose?: boolean) => {
const p = providers[providerID]
const sorted = Object.entries(p.models).sort(([a], [b]) => a.localeCompare(b))
for (const [modelID, model] of sorted) {
@@ -47,7 +47,7 @@ export const ModelsCommand = effectCmd({
}
if (args.provider) {
const providerID = ProviderID.make(args.provider)
const providerID = ProviderV2.ID.make(args.provider)
if (!providers[providerID]) return yield* fail(`Provider not found: ${args.provider}`)
print(providerID, args.verbose)
return
@@ -63,6 +63,6 @@ export const ModelsCommand = effectCmd({
})
// kilocode_change end
for (const providerID of ids) print(ProviderID.make(providerID), args.verbose)
for (const providerID of ids) print(ProviderV2.ID.make(providerID), args.verbose)
}),
})
@@ -1,6 +1,6 @@
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
function promptOffsetWidth(value: string) {
export function promptOffsetWidth(value: string) {
let width = 0
for (const part of graphemes.segment(value)) {
// Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
@@ -479,6 +479,25 @@ export const ProvidersLoginCommand = effectCmd({
)
}
if (provider === "snowflake-cortex") {
const account = yield* promptValue(
yield* Prompt.text({
message: "Snowflake Account Identifier",
placeholder: "xy12345.us-east-1",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
}),
)
const pat = yield* promptValue(
yield* Prompt.password({
message: "Programmatic Access Token (PAT)",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
}),
)
yield* Effect.orDie(authSvc.set(provider, { type: "api", key: pat, metadata: { account } }))
yield* Prompt.outro("Done")
return
}
const key = yield* Prompt.password({
message: "Enter your API key",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
+12 -13
View File
@@ -1,3 +1,4 @@
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
// kilocode_change start - use Kilo CLI branding
// CLI entry point for `kilo run`.
//
@@ -19,15 +20,12 @@ import { pathToFileURL } from "url"
import { Effect } from "effect"
import { UI } from "../ui"
import { effectCmd } from "../effect-cmd"
import { ServerAuth } from "@/server/auth"
import { buildRunMessage } from "@/kilocode/cli/cmd/run-message" // kilocode_change
import { EOL } from "os"
import { Filesystem } from "@/util/filesystem"
import { createKiloClient, type KiloClient, type Session, type ToolPart } from "@kilocode/sdk/v2"
import { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { InstanceRef } from "@/effect/instance-ref"
import { FormatError, FormatUnknownError } from "../error"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
import { event as normalizeEvent } from "./run/event"
@@ -36,7 +34,6 @@ import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change
import { KiloHeadless } from "@/kilocode/permission/headless" // kilocode_change
import { KiloRun, KiloRunDaemon } from "@/kilocode/cli/cmd/run" // kilocode_change
const runtimeTask = import("./run/runtime")
type ModelInput = Parameters<KiloClient["session"]["prompt"]>[0]["model"]
function pick(value: string | undefined): ModelInput | undefined {
@@ -235,8 +232,8 @@ export const RunCommand = effectCmd({
})
.option("replay", {
type: "boolean",
default: false,
describe: "replay visible session history on interactive resume",
default: true,
describe: "replay interactive session history on resume and after resize (use --no-replay to disable)",
})
.option("replay-limit", {
type: "number",
@@ -266,6 +263,10 @@ export const RunCommand = effectCmd({
describe: "enable direct interactive demo slash commands; pass one as the message to run it immediately",
}),
handler: Effect.fn("Cli.run")(function* (args) {
const { Agent } = yield* Effect.promise(() => import("@/agent/agent"))
const { RuntimeFlags } = yield* Effect.promise(() => import("@/effect/runtime-flags"))
const { InstanceRef } = yield* Effect.promise(() => import("@/effect/instance-ref"))
const { ServerAuth } = yield* Effect.promise(() => import("@/server/auth"))
const agentSvc = yield* Agent.Service
const flags = yield* RuntimeFlags.Service
const localInstance = yield* InstanceRef
@@ -298,10 +299,6 @@ export const RunCommand = effectCmd({
die("--interactive cannot be used with --format json")
}
if (args.replay && !args.interactive) {
die("--replay requires --interactive")
}
if (args["replay-limit"] !== undefined && !args.interactive) {
die("--replay-limit requires --interactive")
}
@@ -405,7 +402,7 @@ export const RunCommand = effectCmd({
}
// kilocode_change end
const rules: Permission.Ruleset = args.interactive
const rules: PermissionV1.Ruleset = args.interactive
? []
: [
{
@@ -986,7 +983,7 @@ export const RunCommand = effectCmd({
}
const model = pick(args.model)
const { runInteractiveMode } = await runtimeTask
const { runInteractiveMode } = await import("./run/runtime")
try {
await runInteractiveMode({
sdk: client,
@@ -1003,6 +1000,7 @@ export const RunCommand = effectCmd({
initialInput: input.initial,
createSession: createFreshSession,
thinking,
backgroundSubagents: flags.experimentalBackgroundSubagents,
demo: args.demo,
})
} catch (error) {
@@ -1014,7 +1012,7 @@ export const RunCommand = effectCmd({
if (args.interactive && !args.attach && !args.session && !args.continue) {
await loadInput() // kilocode_change - interactive local mode still consumes its initial input
const model = pick(args.model)
const { runInteractiveLocalMode } = await runtimeTask
const { runInteractiveLocalMode } = await import("./run/runtime")
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const { Server } = await import("@/server/server")
const request = new Request(input, init)
@@ -1037,6 +1035,7 @@ export const RunCommand = effectCmd({
files,
initialInput: input.initial,
thinking,
backgroundSubagents: flags.experimentalBackgroundSubagents,
demo: args.demo,
})
} catch (error) {
+11 -9
View File
@@ -1,3 +1,4 @@
// kilocode_change - new file
import type {
Event as SDKEvent,
GlobalEvent,
@@ -10,25 +11,25 @@ import type {
type MessageUpdated = {
id: string
type: "message.updated"
properties: SyncEventMessageUpdated["data"]
properties: SyncEventMessageUpdated["syncEvent"]["data"]
}
type MessageRemoved = {
id: string
type: "message.removed"
properties: SyncEventMessageRemoved["data"]
properties: SyncEventMessageRemoved["syncEvent"]["data"]
}
type MessagePartUpdated = {
id: string
type: "message.part.updated"
properties: SyncEventMessagePartUpdated["data"]
properties: SyncEventMessagePartUpdated["syncEvent"]["data"]
}
type MessagePartRemoved = {
id: string
type: "message.part.removed"
properties: SyncEventMessagePartRemoved["data"]
properties: SyncEventMessagePartRemoved["syncEvent"]["data"]
}
export type Event = SDKEvent | MessageUpdated | MessageRemoved | MessagePartUpdated | MessagePartRemoved
@@ -36,15 +37,16 @@ export type Event = SDKEvent | MessageUpdated | MessageRemoved | MessagePartUpda
export function event(payload: GlobalEvent["payload"]): Event | undefined {
if (payload.type !== "sync") return payload
switch (payload.name) {
const sync = payload.syncEvent
switch (sync.type) {
case "message.updated.1":
return { id: payload.id, type: "message.updated", properties: payload.data }
return { id: sync.id, type: "message.updated", properties: sync.data }
case "message.removed.1":
return { id: payload.id, type: "message.removed", properties: payload.data }
return { id: sync.id, type: "message.removed", properties: sync.data }
case "message.part.updated.1":
return { id: payload.id, type: "message.part.updated", properties: payload.data }
return { id: sync.id, type: "message.part.updated", properties: sync.data }
case "message.part.removed.1":
return { id: payload.id, type: "message.part.removed", properties: payload.data }
return { id: sync.id, type: "message.part.removed", properties: sync.data }
default:
return undefined
}
@@ -4,9 +4,8 @@ import { useKeyboard, type JSX } from "@opentui/solid"
import fuzzysort from "fuzzysort"
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
import { RunFooterMenu, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
import { formatBindings } from "./keymap.shared"
import type { RunFooterTheme } from "./theme"
import type { FooterKeybinds, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
import type { FooterQueuedPrompt, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
type PanelEntry = RunFooterMenuItem & {
category: string
@@ -15,6 +14,7 @@ type PanelEntry = RunFooterMenuItem & {
type CommandEntry =
| (PanelEntry & { action: "model" })
| (PanelEntry & { action: "queued" })
| (PanelEntry & { action: "subagent" })
| (PanelEntry & { action: "variant.cycle" })
| (PanelEntry & { action: "variant.list" })
@@ -38,6 +38,10 @@ type SubagentEntry = PanelEntry & {
current: boolean
}
type QueuedEntry = PanelEntry & {
prompt: FooterQueuedPrompt
}
type MenuState = ReturnType<typeof createFooterMenuState>
const PANEL_PAD = 2
@@ -295,11 +299,13 @@ export function RunCommandMenuBody(props: {
theme: Accessor<RunFooterTheme>
commands: Accessor<RunCommand[] | undefined>
subagents: Accessor<FooterSubagentTab[]>
queued: Accessor<FooterQueuedPrompt[]>
variants: Accessor<string[]>
keybinds: FooterKeybinds
variantCycle: string
onClose: () => void
onModel: () => void
onSubagent: () => void
onQueued: () => void
onVariant: () => void
onVariantCycle: () => void
onCommand: (name: string) => void
@@ -316,6 +322,20 @@ export function RunCommandMenuBody(props: {
category: "Suggested",
display: "Switch model",
},
...(props.queued().length > 0
? [
{
action: "queued" as const,
category: "Suggested",
display: "Manage queued prompts",
footer: `${props.queued().length} queued`,
keywords: props
.queued()
.map((item) => item.prompt.text)
.join(" "),
},
]
: []),
...(props.subagents().length > 0
? [
{
@@ -334,7 +354,7 @@ export function RunCommandMenuBody(props: {
action: "variant.cycle",
category: "Suggested",
display: "Variant cycle",
footer: formatBindings(props.keybinds.variantCycle, props.keybinds.leader),
footer: props.variantCycle,
keywords: "variant cycle",
},
...(props.variants().length > 0
@@ -388,6 +408,11 @@ export function RunCommandMenuBody(props: {
return
}
if (item.action === "queued") {
props.onQueued()
return
}
if (item.action === "variant.cycle") {
props.onVariantCycle()
return
@@ -560,6 +585,102 @@ export function RunSubagentSelectBody(props: {
)
}
export function RunQueuedPromptSelectBody(props: {
theme: Accessor<RunFooterTheme>
prompts: Accessor<FooterQueuedPrompt[]>
onClose: () => void
onEdit: (prompt: FooterQueuedPrompt) => void | Promise<void>
onDelete: (prompt: FooterQueuedPrompt) => void | Promise<void>
onRows?: (rows: number) => void
}) {
let field: InputRenderable | undefined
const [query, setQuery] = createSignal("")
const entries = createMemo<QueuedEntry[]>(() =>
props.prompts().map((prompt) => ({
category: "",
display: prompt.prompt.text.replaceAll("\n", " "),
footer: "queued · ctrl+e edit · ctrl+d remove",
keywords: prompt.prompt.text,
prompt,
})),
)
const items = createMemo<QueuedEntry[]>(() => match(query(), entries()))
const menu = createFooterMenuState({ count: () => items().length, limit: SUBAGENT_LIST_ROWS })
const selected = () => items()[menu.selected()]
createEffect(() => {
query()
menu.reset()
})
createEffect(() => {
props.onRows?.(menu.rows() + PANEL_FRAME_ROWS)
})
useKeyboard((event) => {
if (event.defaultPrevented) {
return
}
const item = selected()
const ctrl = event.ctrl && !event.meta && !event.shift && !event.super
if (item && (event.name === "delete" || (ctrl && event.name === "d"))) {
event.preventDefault()
props.onDelete(item.prompt)
return
}
if (item && ctrl && event.name === "e") {
event.preventDefault()
props.onEdit(item.prompt)
return
}
handleKey({
event,
menu,
field: () => field,
setQuery,
select: () => {
const item = selected()
if (item) props.onEdit(item.prompt)
},
close: props.onClose,
})
})
return (
<PanelShell
id="run-direct-footer-queued-panel"
title="Queued prompts"
query={query()}
count={items().length}
total={entries().length}
placeholder="Search"
theme={props.theme}
inputRef={(input) => {
field = input
}}
onQuery={setQuery}
>
<RunFooterMenu
id="run-direct-footer-queued-list"
theme={props.theme}
items={items}
selected={menu.selected}
offset={menu.offset}
rows={menu.rows}
limit={SUBAGENT_LIST_ROWS}
empty="No queued prompts"
border={false}
paddingLeft={PANEL_PAD}
paddingRight={PANEL_PAD}
grouped={false}
/>
</PanelShell>
)
}
export function RunVariantSelectBody(props: {
theme: Accessor<RunFooterTheme>
variants: Accessor<string[]>
@@ -64,7 +64,8 @@ function buttons(
)
}
function RejectField(props: {
/** @internal Exported to test managed textarea submission without permission navigation. */
export function RejectField(props: {
theme: RunFooterTheme
text: string
disabled: boolean
@@ -107,6 +108,7 @@ function RejectField(props: {
focusedBackgroundColor={props.theme.surface}
cursorColor={props.theme.text}
focused={!props.disabled}
onSubmit={props.onConfirm}
onContentChange={() => {
if (!area || area.isDestroyed) {
return
@@ -119,11 +121,6 @@ function RejectField(props: {
props.onCancel()
return
}
if (event.name === "return" && !event.meta && !event.ctrl && !event.shift) {
event.preventDefault()
props.onConfirm()
}
}}
ref={(item) => {
area = item
@@ -1,13 +1,13 @@
// Prompt textarea component and its state machine for direct interactive mode.
//
// createPromptState() wires keybinds, history navigation, leader-key sequences,
// and `@` autocomplete for files, subagents, and MCP resources.
// createPromptState() wires keymap command layers, history navigation, and
// `@` autocomplete for files, subagents, and MCP resources.
// It produces a PromptState that RunPromptBody renders as an OpenTUI textarea,
// while the footer view renders the current menu state below it.
/** @jsxImportSource @opentui/solid */
import { pathToFileURL } from "bun"
import { StyledText, bg, fg, type KeyBinding, type KeyEvent, type TextareaRenderable } from "@opentui/core"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { StyledText, bg, fg, type KeyEvent, type TextareaRenderable } from "@opentui/core"
import { useRenderer } from "@opentui/solid"
import fuzzysort from "fuzzysort"
import path from "path"
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
@@ -21,15 +21,12 @@ import {
mentionTriggerIndex,
isNewCommand,
movePromptHistory,
promptCycle,
promptHit,
promptInfo,
promptKeys,
pushPromptHistory,
} from "./prompt.shared"
import { KILO_BASE_MODE, useBindings } from "@/cli/cmd/tui/keymap"
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
import type { RunFooterTheme } from "./theme"
import type { FooterKeybinds, FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunResource } from "./types"
import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunResource, RunTuiConfig } from "./types"
const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS
const AUTOCOMPLETE_BOTTOM_ROWS = 1
@@ -67,10 +64,9 @@ type PromptInput = {
directory: string
findFiles: (query: string) => Promise<string[]>
agents: Accessor<RunAgent[]>
subagents: Accessor<number>
resources: Accessor<RunResource[]>
commands: Accessor<RunCommand[] | undefined>
keybinds: FooterKeybinds
tuiConfig: RunTuiConfig
state: Accessor<FooterState>
view: Accessor<string>
prompt: Accessor<boolean>
@@ -83,14 +79,12 @@ type PromptInput = {
onInputClear: () => void
onExitRequest?: () => boolean
onExit: () => void
onSubagentMenu?: () => void
onRows: (rows: number) => void
onStatus: (text: string) => void
}
export type PromptState = {
placeholder: Accessor<StyledText | string>
bindings: Accessor<KeyBinding[]>
shell: Accessor<boolean>
visible: Accessor<boolean>
options: Accessor<PromptOption[]>
@@ -103,6 +97,7 @@ export type PromptState = {
onKeyDown: (event: KeyEvent) => void
onContentChange: () => void
replaceDraft: (text: string) => void
replacePrompt: (prompt: RunPrompt) => void
bind: (area?: TextareaRenderable) => void
}
@@ -201,7 +196,6 @@ export function hintFlags(width: number) {
export function RunPromptBody(props: {
theme: () => RunFooterTheme
placeholder: () => StyledText | string
bindings: () => KeyBinding[]
onSubmit: () => void
onKeyDown: (event: KeyEvent) => void
onContentChange: () => void
@@ -265,7 +259,6 @@ export function RunPromptBody(props: {
backgroundColor={props.theme().surface}
focusedBackgroundColor={props.theme().surface}
cursorColor={props.theme().text}
keyBindings={props.bindings()}
onSubmit={props.onSubmit}
onKeyDown={props.onKeyDown}
onPaste={() => {
@@ -282,8 +275,6 @@ export function RunPromptBody(props: {
}
export function createPromptState(input: PromptInput): PromptState {
const keys = createMemo(() => promptKeys(input.keybinds))
const bindings = createMemo(() => keys().bindings)
const [shell, setShell] = createSignal(false)
const placeholder = createMemo(() => {
if (shell()) {
@@ -303,8 +294,6 @@ export function createPromptState(input: PromptInput): PromptState {
let draft: RunPrompt = { text: "", parts: [] }
let stash: RunPrompt = { text: "", parts: [] }
let area: TextareaRenderable | undefined
let leader = false
let timeout: NodeJS.Timeout | undefined
let tick = false
let prev = input.view()
let type = 0
@@ -463,24 +452,6 @@ export function createPromptState(input: PromptInput): PromptState {
return visible() ? menu.rows() - 1 + AUTOCOMPLETE_BOTTOM_ROWS : 0
})
const clear = () => {
leader = false
if (!timeout) {
return
}
clearTimeout(timeout)
timeout = undefined
}
const arm = () => {
clear()
leader = true
timeout = setTimeout(() => {
clear()
}, input.keybinds.leaderTimeout)
}
const hide = () => {
setMode(false)
setQuery("")
@@ -744,7 +715,7 @@ export function createPromptState(input: PromptInput): PromptState {
const move = (dir: -1 | 1, event: KeyEvent) => {
if (!area || area.isDestroyed) {
return
return false
}
if (history.index === null && dir === -1) {
@@ -753,7 +724,7 @@ export function createPromptState(input: PromptInput): PromptState {
const next = movePromptHistory(history, dir, area.plainText, area.cursorOffset)
if (!next.apply || next.text === undefined || next.cursor === undefined) {
return
return false
}
history = next.state
@@ -761,28 +732,27 @@ export function createPromptState(input: PromptInput): PromptState {
next.state.index === null ? stash : (next.state.items[next.state.index] ?? { text: next.text, parts: [] })
restore(value, next.cursor)
event.preventDefault()
return true
}
const cycle = (event: KeyEvent): boolean => {
const next = promptCycle(leader, promptInfo(event), keys().leaders, keys().cycles)
if (!next.consume) {
return false
const historyCommand = (dir: -1 | 1, event: KeyEvent) => {
if (move(dir, event)) return
if (!area || area.isDestroyed) return false
const endOffset = Bun.stringWidth(area.plainText)
if (dir === -1 && area.visualCursor.visualRow === 0) {
area.cursorOffset = 0
}
if (next.clear) {
clear()
const end =
typeof area.height === "number" && Number.isFinite(area.height) && area.height > 0
? area.height - 1
: Math.max(0, (area.virtualLineCount ?? 1) - 1)
if (dir === 1 && area.visualCursor.visualRow === end) {
area.cursorOffset = endOffset
}
if (next.arm) {
arm()
}
if (next.cycle) {
input.onCycle()
}
event.preventDefault()
return true
return false
}
const requestExit = () => {
@@ -822,12 +792,20 @@ export function createPromptState(input: PromptInput): PromptState {
}
if (next.kind === "slash") {
const text = `${next.display} ` // kilocode_change
const cursor = area.cursorOffset
const head = slashHead(area.plainText)
const local = !shell() && (next.name === "new" || next.name === "exit")
const separator = !shell() && !local && head && /\s/.test(area.plainText[head.end] ?? "") ? "" : " "
const text = `${next.display}${separator}` // kilocode_change
area.cursorOffset = 0
const start = area.logicalCursor
area.cursorOffset = cursor
area.cursorOffset =
shell() || !head
? cursor
: local
? Bun.stringWidth(area.plainText)
: Bun.stringWidth(area.plainText.slice(0, head.end))
const end = area.logicalCursor
area.deleteRange(start.row, start.col, end.row, end.col)
@@ -835,6 +813,11 @@ export function createPromptState(input: PromptInput): PromptState {
area.cursorOffset = Bun.stringWidth(text)
hide()
syncDraft()
if (!shell()) {
submitPrompt(clonePrompt(draft))
return
}
scheduleRows()
area.focus()
return
@@ -914,178 +897,180 @@ export function createPromptState(input: PromptInput): PromptState {
refresh()
}
const onKeyDown = (event: KeyEvent) => {
const key = promptInfo(event)
if (visible()) {
const name = event.name.toLowerCase()
const ctrl = event.ctrl && !event.meta && !event.shift
if (name === "up" || (ctrl && name === "p")) {
event.preventDefault()
if (options().length > 0) {
menu.move(-1)
}
return
}
if (name === "down" || (ctrl && name === "n")) {
event.preventDefault()
if (options().length > 0) {
menu.move(1)
}
return
}
if (name === "escape") {
event.preventDefault()
cancelAutocomplete()
return
}
if (name === "return") {
if (mode() === "slash" && options().length === 0) {
hide()
return
}
event.preventDefault()
select()
return
}
if (name === "tab") {
if (mode() === "slash" && options().length === 0) {
hide()
return
}
event.preventDefault()
const item = options()[menu.selected()]
if (item?.kind === "mention" && item.directory) {
expand()
return
}
select()
return
}
}
if (
key.name === "!" &&
!shell() &&
!event.ctrl &&
!event.meta &&
!event.super &&
area &&
!area.isDestroyed &&
area.cursorOffset === 0
) {
event.preventDefault()
setShellMode(true)
return
}
if (shell() && !visible()) {
if (key.name === "escape") {
event.preventDefault()
setShellMode(false)
return
}
if (key.name === "backspace" && area && !area.isDestroyed && area.cursorOffset === 0) {
event.preventDefault()
setShellMode(false)
return
}
}
if (
key.name === "down" &&
!visible() &&
!event.ctrl &&
!event.meta &&
!event.shift &&
!event.super &&
area &&
!area.isDestroyed &&
area.plainText.length === 0 &&
input.subagents() > 0
) {
event.preventDefault()
input.onSubagentMenu?.()
return
}
if (promptHit(keys().clear, key)) {
const handled = requestExit()
if (handled) {
event.preventDefault()
}
return
}
if (promptHit(keys().interrupts, key)) {
if (input.onInterrupt()) {
event.preventDefault()
return
}
}
if (cycle(event)) {
return
}
const up = promptHit(keys().previous, key)
const down = promptHit(keys().next, key)
if (!up && !down) {
return
}
if (!area || area.isDestroyed) {
return
}
const dir = up ? -1 : 1
const endOffset = Bun.stringWidth(area.plainText)
if ((dir === -1 && area.cursorOffset === 0) || (dir === 1 && area.cursorOffset === endOffset)) {
move(dir, event)
return
}
if (dir === -1 && area.visualCursor.visualRow === 0) {
area.cursorOffset = 0
}
const end =
typeof area.height === "number" && Number.isFinite(area.height) && area.height > 0
? area.height - 1
: Math.max(0, (area.virtualLineCount ?? 1) - 1)
if (dir === 1 && area.visualCursor.visualRow === end) {
area.cursorOffset = endOffset
}
const baseBindingsEnabled = () => {
const current = input.view()
if (current === "command") return false
if (current === "model") return false
if (current === "variant") return false
if (current === "queued-menu") return false
if (current === "subagent-menu") return false
return true
}
useKeyboard((event) => {
if (input.prompt()) {
return
}
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: baseBindingsEnabled(),
commands: [
{
name: "prompt.clear",
title: "Clear prompt or exit",
category: "Prompt",
run() {
if (requestExit()) return
return false
},
},
],
bindings: input.tuiConfig.keybinds.get("prompt.clear"),
}))
if (
input.view() === "command" ||
input.view() === "model" ||
input.view() === "variant" ||
input.view() === "subagent-menu"
) {
return
}
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: input.prompt(),
commands: [
{
name: "session.interrupt",
title: "Interrupt session",
category: "Session",
run() {
if (input.onInterrupt()) return
return false
},
},
],
bindings: input.tuiConfig.keybinds.get("session.interrupt"),
}))
if (promptHit(keys().clear, promptInfo(event))) {
const handled = requestExit()
if (handled) {
event.preventDefault()
}
}
})
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: input.prompt() && !visible(),
commands: [
{
name: "prompt.history.previous",
title: "Previous prompt history",
category: "Prompt",
run(ctx: { event: KeyEvent }) {
return historyCommand(-1, ctx.event)
},
},
{
name: "prompt.history.next",
title: "Next prompt history",
category: "Prompt",
run(ctx: { event: KeyEvent }) {
return historyCommand(1, ctx.event)
},
},
],
bindings: [
...input.tuiConfig.keybinds.get("prompt.history.previous"),
...input.tuiConfig.keybinds.get("prompt.history.next"),
],
}))
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: input.prompt() && !visible(),
bindings: [
{
key: "!",
desc: "Shell mode",
group: "Prompt",
cmd() {
if (shell()) return false
if (!area || area.isDestroyed) return false
if (area.cursorOffset !== 0) return false
setShellMode(true)
},
},
],
}))
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: input.prompt() && shell() && !visible(),
bindings: [
{
key: "escape",
desc: "Exit shell mode",
group: "Prompt",
cmd: () => setShellMode(false),
},
{
key: "backspace",
desc: "Exit shell mode",
group: "Prompt",
cmd() {
if (!area || area.isDestroyed) return false
if (area.cursorOffset !== 0) return false
setShellMode(false)
},
},
],
}))
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: input.prompt() && visible(),
commands: [
{
name: "prompt.autocomplete.prev",
title: "Previous autocomplete item",
category: "Autocomplete",
run: () => menu.move(-1),
},
{
name: "prompt.autocomplete.next",
title: "Next autocomplete item",
category: "Autocomplete",
run: () => menu.move(1),
},
{
name: "prompt.autocomplete.hide",
title: "Hide autocomplete",
category: "Autocomplete",
run: cancelAutocomplete,
},
{
name: "prompt.autocomplete.select",
title: "Select autocomplete item",
category: "Autocomplete",
run() {
if (mode() === "slash" && options().length === 0) {
hide()
return
}
select()
},
},
{
name: "prompt.autocomplete.complete",
title: "Complete autocomplete item",
category: "Autocomplete",
run() {
if (mode() === "slash" && options().length === 0) {
hide()
return
}
const item = options()[menu.selected()]
if (item?.kind === "mention" && item.directory) {
expand()
return
}
select()
},
},
],
bindings: input.tuiConfig.keybinds.gather("run.prompt.autocomplete", [
"prompt.autocomplete.prev",
"prompt.autocomplete.next",
"prompt.autocomplete.hide",
"prompt.autocomplete.select",
"prompt.autocomplete.complete",
]),
}))
const onKeyDown = (_event: KeyEvent) => {}
const submitPrompt = (next: RunPrompt) => {
if (!area || area.isDestroyed) {
@@ -1146,7 +1131,6 @@ export function createPromptState(input: PromptInput): PromptState {
}
onCleanup(() => {
clear()
if (area && !area.isDestroyed) {
area.off("line-info-change", scheduleRows)
}
@@ -1190,7 +1174,6 @@ export function createPromptState(input: PromptInput): PromptState {
syncDraft()
}
clear()
hide()
prev = kind
if (kind !== "prompt") {
@@ -1204,7 +1187,6 @@ export function createPromptState(input: PromptInput): PromptState {
return {
placeholder,
bindings,
shell,
visible,
options,
@@ -1221,6 +1203,7 @@ export function createPromptState(input: PromptInput): PromptState {
scheduleRows()
},
replaceDraft,
replacePrompt: restore,
bind,
}
}
@@ -177,10 +177,6 @@ export function RunQuestionBody(props: {
return
}
if (event.name === "return" && !event.shift && !event.ctrl && !event.meta) {
saveCustom()
event.preventDefault()
}
return
}
@@ -496,6 +492,7 @@ export function RunQuestionBody(props: {
focusedBackgroundColor={props.theme.surface}
cursorColor={props.theme.text}
focused={!disabled()}
onSubmit={saveCustom}
onContentChange={() => {
if (!area || area.isDestroyed || disabled()) {
return
+256 -59
View File
@@ -24,25 +24,26 @@
// Ctrl-c clears a live prompt draft first; otherwise interrupt and exit use a
// two-press pattern where the first press shows a hint and the second press
// within 5 seconds actually fires the action.
import { CliRenderEvents, type CliRenderer, type TreeSitterClient } from "@opentui/core"
import { CliRenderEvents, type CliRenderer, type KeyEvent, type Renderable, type TreeSitterClient } from "@opentui/core"
import type { Keymap } from "@opentui/keymap"
import { render } from "@opentui/solid"
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { OpencodeKeymapProvider, formatKeyBindings } from "@/cli/cmd/tui/keymap"
import { withRunSpan } from "./otel"
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
import { RUN_INTERACTIVE_TERMINAL_ROWS } from "@/kilocode/cli/cmd/run/interactive-terminal" // kilocode_change
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
import { printableBinding } from "./prompt.shared"
import { RunFooterView } from "./footer.view"
import { RunScrollbackStream } from "./scrollback.surface"
import type { RunTheme } from "./theme"
import { RUN_THEME_FALLBACK, resolveRunTheme, type RunTheme } from "./theme"
import type {
FooterApi,
FooterEvent,
FooterKeybinds,
FooterPatch,
FooterPromptRoute,
FooterQueuedPrompt,
FooterState,
FooterSubagentState,
FooterView,
@@ -56,6 +57,7 @@ import type {
RunPrompt,
RunProvider,
RunResource,
RunTuiConfig,
StreamCommit,
} from "./types"
@@ -81,7 +83,9 @@ type RunFooterOptions = {
first: boolean
history?: RunPrompt[]
theme: RunTheme
keybinds: FooterKeybinds
keymap: Keymap<Renderable, KeyEvent>
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
diffStyle: RunDiffStyle
onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void>
@@ -93,6 +97,7 @@ type RunFooterOptions = {
onModelSelect?: (model: NonNullable<RunInput["model"]>) => CycleResult | void | Promise<CycleResult | void>
onVariantSelect?: (variant: string | undefined) => CycleResult | void | Promise<CycleResult | void>
onInterrupt?: () => void
onBackground?: () => void
onExit?: () => void
onSubagentSelect?: (sessionID: string | undefined) => void
treeSitterClient?: TreeSitterClient
@@ -105,6 +110,7 @@ const SUBAGENT_ROWS = RUN_SUBAGENT_PANEL_ROWS
const MODEL_ROWS = RUN_COMMAND_PANEL_ROWS
const VARIANT_ROWS = RUN_COMMAND_PANEL_ROWS
const AUTOCOMPLETE_COMPACT_ROWS = 2
const THEME_REFRESH_DELAYS = [1000, 1000] as const
function createEmptySubagentState(): FooterSubagentState {
return {
@@ -166,11 +172,13 @@ export class RunFooter implements FooterApi {
private closed = false
private destroyed = false
private prompts = new Set<(input: RunPrompt) => void>()
private queuedRemoves = new Set<(messageID: string) => boolean | Promise<boolean>>()
private closes = new Set<() => void>()
// Microtask-coalesced commit queue. Flushed on next microtask or on close/destroy.
private queue: StreamCommit[] = []
private pending = false
private flushing: Promise<void> = Promise.resolve()
private flushError: unknown
// Fixed portion of footer height above the textarea.
private base: number
private rows = TEXTAREA_MIN_ROWS
@@ -188,20 +196,42 @@ export class RunFooter implements FooterApi {
private setVariants: Setter<string[]>
private currentVariant: Accessor<string | undefined>
private setCurrentVariant: Setter<string | undefined>
private theme: Accessor<RunTheme>
private setTheme: Setter<RunTheme>
private state: Accessor<FooterState>
private setState: Setter<FooterState>
private view: Accessor<FooterView>
private setView: Setter<FooterView>
private subagent: Accessor<FooterSubagentState>
private setSubagent: (next: FooterSubagentState) => void
private queuedPrompts: Accessor<FooterQueuedPrompt[]>
private setQueuedPrompts: Setter<FooterQueuedPrompt[]>
private promptRoute: FooterPromptRoute = { type: "composer" }
private subagentMenuRows = SUBAGENT_ROWS
private autocomplete = false
private interruptTimeout: NodeJS.Timeout | undefined
private exitTimeout: NodeJS.Timeout | undefined
private interruptHint: string
private requestExitHandler: (() => boolean) | undefined
private scrollback: RunScrollbackStream
private themes: RunTheme[]
private paletteRefreshRunning = false
private paletteRefreshQueued = false
private themeRefreshTimeouts: NodeJS.Timeout[] = []
private createScrollback(wrote: boolean): RunScrollbackStream {
return new RunScrollbackStream(this.renderer, this.theme(), {
diffStyle: this.options.diffStyle,
wrote,
sessionID: this.options.sessionID,
treeSitterClient: this.options.treeSitterClient,
onThemeRelease: (theme) => {
void this.renderer
.idle()
.catch(() => {})
.finally(() => this.destroyTheme(theme))
},
})
}
constructor(
private renderer: CliRenderer,
@@ -244,6 +274,10 @@ export class RunFooter implements FooterApi {
const [currentVariant, setCurrentVariant] = createSignal(options.variant)
this.currentVariant = currentVariant
this.setCurrentVariant = setCurrentVariant
const [theme, setTheme] = createSignal(options.theme)
this.theme = theme
this.setTheme = setTheme
this.themes = [options.theme]
const [subagent, setSubagent] = createStore<FooterSubagentState>(createEmptySubagentState())
this.subagent = () => subagent
this.setSubagent = (next) => {
@@ -252,58 +286,69 @@ export class RunFooter implements FooterApi {
setSubagent("permissions", reconcile(next.permissions, { key: "id" }))
setSubagent("questions", reconcile(next.questions, { key: "id" }))
}
const [queuedPrompts, setQueuedPrompts] = createSignal<FooterQueuedPrompt[]>([])
this.queuedPrompts = queuedPrompts
this.setQueuedPrompts = setQueuedPrompts
this.base = Math.max(1, renderer.footerHeight - TEXTAREA_MIN_ROWS)
this.interruptHint = printableBinding(options.keybinds.interrupt, options.keybinds.leader) || "esc"
this.scrollback = new RunScrollbackStream(renderer, options.theme, {
diffStyle: options.diffStyle,
wrote: options.wrote,
sessionID: options.sessionID,
treeSitterClient: options.treeSitterClient,
})
this.scrollback = this.createScrollback(options.wrote ?? false)
this.renderer.on(CliRenderEvents.DESTROY, this.handleDestroy)
this.renderer.on(CliRenderEvents.PALETTE, this.handlePalette)
this.renderer.on(CliRenderEvents.THEME_MODE, this.handleThemeRefresh)
this.renderer.prependInputHandler(this.handleThemeNotification)
process.on("SIGUSR2", this.handleThemeSignal)
const footer = this
void render(
() =>
createComponent(RunFooterView, {
directory: options.directory,
state: this.state,
view: this.view,
subagent: this.subagent,
findFiles: options.findFiles,
agents: this.agents,
resources: this.resources,
commands: this.commands,
providers: this.providers,
currentModel: this.currentModel,
variants: this.variants,
currentVariant: this.currentVariant,
theme: options.theme,
diffStyle: options.diffStyle,
keybinds: options.keybinds,
history: options.history,
agent: options.agentLabel,
onSubmit: this.handlePrompt,
onPermissionReply: this.handlePermissionReply,
onQuestionReply: this.handleQuestionReply,
onQuestionReject: this.handleQuestionReject,
// kilocode_change start
onTerminalWrite: options.onTerminalWrite,
onTerminalResize: options.onTerminalResize,
onTerminalClose: options.onTerminalClose,
// kilocode_change end
onCycle: this.handleCycle,
onInterrupt: this.handleInterrupt,
onInputClear: this.handleInputClear,
onExitRequest: this.handleExit,
onRequestExit: this.setRequestExitHandler,
onExit: () => this.close(),
onModelSelect: this.handleModelSelect,
onVariantSelect: this.handleVariantSelect,
onRows: this.syncRows,
onLayout: this.syncLayout,
onStatus: this.setStatus,
onSubagentSelect: options.onSubagentSelect,
createComponent(OpencodeKeymapProvider, {
keymap: options.keymap,
get children() {
return createComponent(RunFooterView, {
directory: options.directory,
state: footer.state,
view: footer.view,
subagent: footer.subagent,
queuedPrompts: footer.queuedPrompts,
findFiles: options.findFiles,
agents: footer.agents,
resources: footer.resources,
commands: footer.commands,
providers: footer.providers,
currentModel: footer.currentModel,
variants: footer.variants,
currentVariant: footer.currentVariant,
theme: footer.theme,
diffStyle: options.diffStyle,
tuiConfig: options.tuiConfig,
backgroundSubagents: options.backgroundSubagents,
history: options.history,
agent: options.agentLabel,
onSubmit: footer.handlePrompt,
onPermissionReply: footer.handlePermissionReply,
onQuestionReply: footer.handleQuestionReply,
onQuestionReject: footer.handleQuestionReject,
// kilocode_change start
onTerminalWrite: options.onTerminalWrite,
onTerminalResize: options.onTerminalResize,
onTerminalClose: options.onTerminalClose,
// kilocode_change end
onCycle: footer.handleCycle,
onInterrupt: footer.handleInterrupt,
onBackground: options.onBackground,
onInputClear: footer.handleInputClear,
onExitRequest: footer.handleExit,
onRequestExit: footer.setRequestExitHandler,
onExit: () => footer.close(),
onModelSelect: footer.handleModelSelect,
onVariantSelect: footer.handleVariantSelect,
onRows: footer.syncRows,
onLayout: footer.syncLayout,
onStatus: footer.setStatus,
onSubagentSelect: options.onSubagentSelect,
onQueuedRemove: footer.handleQueuedRemove,
})
},
}),
this.renderer,
).catch(() => {
@@ -328,6 +373,13 @@ export class RunFooter implements FooterApi {
}
}
public onQueuedRemove(fn: (messageID: string) => boolean | Promise<boolean>): () => void {
this.queuedRemoves.add(fn)
return () => {
this.queuedRemoves.delete(fn)
}
}
public onClose(fn: () => void): () => void {
if (this.isClosed) {
fn()
@@ -373,6 +425,15 @@ export class RunFooter implements FooterApi {
return
}
if (next.type === "queued.prompts") {
if (this.isGone) {
return
}
this.setQueuedPrompts(next.prompts)
return
}
const patch = eventPatch(next)
if (patch) {
this.patch(patch)
@@ -443,7 +504,9 @@ export class RunFooter implements FooterApi {
},
),
)
.catch(() => {})
.catch((error) => {
this.flushError = error
})
}
private present(view: FooterView): void {
@@ -501,6 +564,12 @@ export class RunFooter implements FooterApi {
}
return this.flushing.then(async () => {
if (this.flushError !== undefined) {
const error = this.flushError
this.flushError = undefined
throw error
}
if (this.isGone) {
return
}
@@ -513,6 +582,30 @@ export class RunFooter implements FooterApi {
})
}
public resetForReplay(wrote: boolean): void {
if (this.isGone) {
return
}
this.scrollback.destroy()
this.scrollback = this.createScrollback(wrote)
}
public currentTheme(): RunTheme {
return this.theme()
}
private destroyTheme(theme: RunTheme): void {
const index = this.themes.indexOf(theme)
if (index === -1) {
return
}
this.themes.splice(index, 1)
theme.block.syntax?.destroy()
theme.block.subtleSyntax?.destroy()
}
public close(): void {
if (this.closed) {
return
@@ -549,6 +642,11 @@ export class RunFooter implements FooterApi {
this.requestExitHandler = fn
}
private handleQueuedRemove = async (messageID: string): Promise<boolean> => {
const fn = [...this.queuedRemoves][0]
return fn ? await fn(messageID) : false
}
private handleInputClear = (): void => {
this.clearInterruptTimer()
this.clearExitTimer()
@@ -580,11 +678,13 @@ export class RunFooter implements FooterApi {
? 1 + MODEL_ROWS
: this.promptRoute.type === "variant"
? 1 + VARIANT_ROWS
: this.promptRoute.type === "subagent-menu"
: this.promptRoute.type === "queued-menu"
? 1 + this.subagentMenuRows
: this.promptRoute.type === "subagent"
? this.base + SUBAGENT_INSPECTOR_ROWS
: Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
: this.promptRoute.type === "subagent-menu"
? 1 + this.subagentMenuRows
: this.promptRoute.type === "subagent"
? this.base + SUBAGENT_INSPECTOR_ROWS
: Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
if (height !== this.renderer.footerHeight) {
this.renderer.footerHeight = height
@@ -692,7 +792,11 @@ export class RunFooter implements FooterApi {
return
}
const previous = this.currentModel()
this.setCurrentModel(model)
if (!previous || previous.providerID !== model.providerID || previous.modelID !== model.modelID) {
this.setCurrentVariant(undefined)
}
void Promise.resolve()
.then(() => this.options.onModelSelect?.(model))
.then((result) => {
@@ -794,6 +898,13 @@ export class RunFooter implements FooterApi {
}, 5000)
}
private interruptHint(): string {
const bindings = this.options.keymap
.getCommandBindings({ visibility: "registered", commands: ["session.interrupt"] })
.get("session.interrupt")
return formatKeyBindings(bindings, this.options.tuiConfig) || "esc"
}
private clearExitTimer(): void {
if (!this.exitTimeout) {
return
@@ -828,7 +939,7 @@ export class RunFooter implements FooterApi {
if (next < 2) {
this.armInterruptTimer()
this.patch({ status: `${this.interruptHint} again to interrupt` })
this.patch({ status: `${this.interruptHint()} again to interrupt` })
return true
}
@@ -860,6 +971,82 @@ export class RunFooter implements FooterApi {
return true
}
private handlePalette = (): void => {
void resolveRunTheme(this.renderer).then((theme) => {
if (this.isGone) {
theme.block.syntax?.destroy()
theme.block.subtleSyntax?.destroy()
return
}
// Keep the last known good theme when a runtime OSC probe times out.
if (theme === RUN_THEME_FALLBACK) {
return
}
this.themes.push(theme)
this.setTheme(theme)
this.renderer.setBackgroundColor(theme.background)
this.flushing = this.flushing
.then(() => this.scrollback.setTheme(theme))
.catch((error) => {
this.flushError = error
})
})
}
private handleThemeNotification = (sequence: string): boolean => {
if (sequence !== "\x1b[?997;1n" && sequence !== "\x1b[?997;2n") {
return false
}
// OpenTUI clears its palette cache only when dark/light mode changes.
// Refresh for same-mode terminal theme swaps too.
queueMicrotask(this.handleThemeRefresh)
return false
}
private handleThemeRefresh = (): void => {
if (this.isGone) {
return
}
if (this.paletteRefreshRunning) {
this.paletteRefreshQueued = true
return
}
this.paletteRefreshRunning = true
const retry = this.renderer.paletteDetectionStatus === "detecting"
this.renderer.clearPaletteCache()
void this.renderer
.getPalette({ size: 256 })
.catch(() => {})
.finally(() => {
this.paletteRefreshRunning = false
if (!retry && !this.paletteRefreshQueued) {
return
}
this.paletteRefreshQueued = false
this.handleThemeRefresh()
})
}
public refreshTheme(): void {
this.handleThemeRefresh()
}
private handleThemeSignal = (): void => {
// Omarchy signals immediately after requesting a terminal config reload.
for (const timeout of this.themeRefreshTimeouts) clearTimeout(timeout)
this.themeRefreshTimeouts = THEME_REFRESH_DELAYS.map((delay) =>
setTimeout(() => {
this.handleThemeRefresh()
}, delay),
)
}
private handleDestroy = (): void => {
if (this.destroyed) {
return
@@ -871,9 +1058,17 @@ export class RunFooter implements FooterApi {
this.clearInterruptTimer()
this.clearExitTimer()
this.renderer.off(CliRenderEvents.DESTROY, this.handleDestroy)
this.renderer.off(CliRenderEvents.PALETTE, this.handlePalette)
this.renderer.off(CliRenderEvents.THEME_MODE, this.handleThemeRefresh)
this.renderer.removeInputHandler(this.handleThemeNotification)
process.off("SIGUSR2", this.handleThemeSignal)
for (const timeout of this.themeRefreshTimeouts) clearTimeout(timeout)
this.themeRefreshTimeouts.length = 0
this.prompts.clear()
this.queuedRemoves.clear()
this.closes.clear()
this.scrollback.destroy()
for (const theme of [...this.themes]) this.destroyTheme(theme)
}
// Drains the commit queue to scrollback. The surface manager owns grouping,
@@ -903,6 +1098,8 @@ export class RunFooter implements FooterApi {
},
),
)
.catch(() => {})
.catch((error) => {
this.flushError = error
})
}
}
+231 -57
View File
@@ -10,7 +10,7 @@
// All state comes from the parent RunFooter through SolidJS signals.
// The view itself is stateless except for derived memos.
/** @jsxImportSource @opentui/solid */
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { useTerminalDimensions } from "@opentui/solid"
import { Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import "opentui-spinner/solid"
import { createColors, createFrames } from "../tui/ui/spinner"
@@ -19,6 +19,7 @@ import {
RUN_SUBAGENT_PANEL_ROWS,
RunCommandMenuBody,
RunModelSelectBody,
RunQueuedPromptSelectBody,
RunSubagentSelectBody,
RunVariantSelectBody,
} from "./footer.command"
@@ -27,10 +28,16 @@ import { RunFooterSubagentBody } from "./footer.subagent"
import { RunPromptBody, createPromptState, hintFlags } from "./footer.prompt"
import { RunPermissionBody } from "./footer.permission"
import { RunQuestionBody } from "./footer.question"
import { printableBinding, promptBindings, promptHit, promptInfo } from "./prompt.shared"
import {
KILO_BASE_MODE,
formatKeyBindings,
useBindings,
useKeymapSelector,
type OpenTuiKeymap,
} from "@/cli/cmd/tui/keymap"
import type {
FooterKeybinds,
FooterPromptRoute,
FooterQueuedPrompt,
FooterState,
FooterSubagentState,
FooterView,
@@ -44,8 +51,10 @@ import type {
RunPrompt,
RunProvider,
RunResource,
RunTuiConfig,
} from "./types"
import { RUN_THEME_FALLBACK, type RunTheme } from "./theme"
import type { RunTheme } from "./theme"
import { modelInfo } from "./variant.shared"
const EMPTY_BORDER = {
topLeft: "",
@@ -74,9 +83,11 @@ type RunFooterViewProps = {
state: () => FooterState
view?: () => FooterView
subagent?: () => FooterSubagentState
theme?: RunTheme
queuedPrompts?: () => FooterQueuedPrompt[]
theme: () => RunTheme
diffStyle?: RunDiffStyle
keybinds: FooterKeybinds
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
history?: RunPrompt[]
agent: string
onSubmit: (input: RunPrompt) => boolean
@@ -88,6 +99,7 @@ type RunFooterViewProps = {
onTerminalClose: (terminalID: string) => Promise<void> // kilocode_change
onCycle: () => void
onInterrupt: () => boolean
onBackground?: () => void
onInputClear: () => void
onExitRequest?: () => boolean
onRequestExit?: (fn: (() => boolean) | undefined) => void
@@ -98,6 +110,7 @@ type RunFooterViewProps = {
onLayout: (input: { route: FooterPromptRoute; autocomplete: boolean; subagentRows: number }) => void
onStatus: (text: string) => void
onSubagentSelect?: (sessionID: string | undefined) => void
onQueuedRemove: (messageID: string) => Promise<boolean>
}
export { TEXTAREA_MIN_ROWS, TEXTAREA_MAX_ROWS } from "./footer.prompt"
@@ -117,13 +130,15 @@ export function RunFooterView(props: RunFooterViewProps) {
})
const [route, setRoute] = createSignal<FooterPromptRoute>({ type: "composer" })
const [subagentMenuRows, setSubagentMenuRows] = createSignal(RUN_SUBAGENT_PANEL_ROWS)
const queuedPrompts = createMemo(() => props.queuedPrompts?.() ?? [])
const prompt = createMemo(() => active().type === "prompt" && route().type === "composer")
const selectingSubagent = createMemo(() => active().type === "prompt" && route().type === "subagent-menu")
const selectingQueued = createMemo(() => active().type === "prompt" && route().type === "queued-menu")
const inspecting = createMemo(() => active().type === "prompt" && route().type === "subagent")
const commanding = createMemo(() => active().type === "prompt" && route().type === "command")
const modeling = createMemo(() => active().type === "prompt" && route().type === "model")
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
const panel = createMemo(() => selectingSubagent() || commanding() || modeling() || varianting())
const panel = createMemo(() => selectingQueued() || selectingSubagent() || commanding() || modeling() || varianting())
const selected = createMemo(() => {
const current = route()
return current.type === "subagent" ? current.sessionID : undefined
@@ -149,22 +164,84 @@ export function RunFooterView(props: RunFooterViewProps) {
label: count === 1 ? "agent" : "agents",
}
})
const foregroundSubagents = createMemo(
() => props.backgroundSubagents && tabs().some((item) => item.status === "running" && !item.background),
)
const queuedIndicator = createMemo(() => {
const count = queuedPrompts().length
if (count === 0) return
return { count }
})
const model = createMemo(() => {
const current = props.currentModel()
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
})
const detail = createMemo(() => {
const current = route()
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
})
const command = createMemo(() => printableBinding(props.keybinds.commandList, props.keybinds.leader))
const interrupt = createMemo(() => printableBinding(props.keybinds.interrupt, props.keybinds.leader))
const commandKeys = createMemo(() => promptBindings(props.keybinds.commandList, props.keybinds.leader))
const command = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap
.getCommandBindings({ visibility: "registered", commands: ["command.palette.show"] })
.get("command.palette.show"),
props.tuiConfig,
) ?? "",
)
const interrupt = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap
.getCommandBindings({ visibility: "registered", commands: ["session.interrupt"] })
.get("session.interrupt"),
props.tuiConfig,
) ?? "",
)
const variantCycle = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap.getCommandBindings({ visibility: "registered", commands: ["variant.cycle"] }).get("variant.cycle"),
props.tuiConfig,
) ?? "",
)
const queuedShortcut = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap
.getCommandBindings({ visibility: "registered", commands: ["session.queued_prompts"] })
.get("session.queued_prompts"),
props.tuiConfig,
) ?? "",
)
const subagentShortcut = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap
.getCommandBindings({ visibility: "registered", commands: ["session.child.first"] })
.get("session.child.first"),
props.tuiConfig,
) ?? "",
)
const backgroundShortcut = useKeymapSelector(
(keymap: OpenTuiKeymap) =>
formatKeyBindings(
keymap
.getCommandBindings({ visibility: "registered", commands: ["session.background"] })
.get("session.background"),
props.tuiConfig,
) ?? "",
)
const hints = createMemo(() => hintFlags(term().width))
const busy = createMemo(() => props.state().phase === "running")
const armed = createMemo(() => props.state().interrupt > 0)
const exiting = createMemo(() => props.state().exit > 0)
const queue = createMemo(() => props.state().queue)
const additionalQueue = createMemo(() => Math.max(0, queue() - queuedPrompts().length))
const duration = createMemo(() => props.state().duration)
const usage = createMemo(() => props.state().usage)
const interruptKey = createMemo(() => interrupt() || "/exit")
const runTheme = createMemo(() => props.theme ?? RUN_THEME_FALLBACK)
const runTheme = createMemo(() => props.theme())
const theme = createMemo(() => runTheme().footer)
const block = createMemo(() => runTheme().block)
const spin = createMemo(() => {
@@ -230,6 +307,12 @@ export function RunFooterView(props: RunFooterViewProps) {
props.onSubagentSelect?.(undefined)
}
const openQueuedMenu = () => {
if (queuedPrompts().length === 0) return
setRoute({ type: "queued-menu" })
props.onSubagentSelect?.(undefined)
}
const closePanel = () => {
setRoute({ type: "composer" })
}
@@ -264,10 +347,9 @@ export function RunFooterView(props: RunFooterViewProps) {
directory: props.directory,
findFiles: props.findFiles,
agents: props.agents,
subagents: () => tabs().length,
resources: props.resources,
commands: props.commands,
keybinds: props.keybinds,
tuiConfig: props.tuiConfig,
state: props.state,
view: promptView,
prompt,
@@ -280,7 +362,6 @@ export function RunFooterView(props: RunFooterViewProps) {
onInputClear: props.onInputClear,
onExitRequest: props.onExitRequest,
onExit: props.onExit,
onSubagentMenu: openSubagentMenu,
onRows: props.onRows,
onStatus: props.onStatus,
})
@@ -295,30 +376,71 @@ export function RunFooterView(props: RunFooterViewProps) {
props.onRequestExit?.(undefined)
})
useKeyboard((event) => {
if (event.defaultPrevented) {
return
}
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(),
commands: [
{
name: "command.palette.show",
title: "Open command palette",
category: "Prompt",
run: openCommand,
},
{
name: "variant.cycle",
title: "Cycle model variant",
category: "Model",
run: props.onCycle,
},
],
bindings: [
...props.tuiConfig.keybinds.get("command.palette.show"),
...props.tuiConfig.keybinds.get("variant.cycle"),
],
}))
if (active().type !== "prompt") {
return
}
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents(),
priority: 1,
commands: [
{
name: "session.background",
title: "Background subagents",
category: "Session",
run: () => props.onBackground?.(),
},
],
bindings: props.tuiConfig.keybinds.get("session.background"),
}))
if (route().type !== "composer") {
return
}
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0,
commands: [
{
name: "session.child.first",
title: "View subagents",
category: "Session",
run: openSubagentMenu,
},
],
bindings: props.tuiConfig.keybinds.get("session.child.first"),
}))
if (composer.visible()) {
return
}
if (!promptHit(commandKeys(), promptInfo(event))) {
return
}
event.preventDefault()
openCommand()
})
useBindings(() => ({
mode: KILO_BASE_MODE,
enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0,
commands: [
{
name: "session.queued_prompts",
title: "Manage queued prompts",
category: "Session",
run: openQueuedMenu,
},
],
bindings: props.tuiConfig.keybinds.get("session.queued_prompts"),
}))
createEffect(() => {
const current = route()
@@ -345,6 +467,11 @@ export function RunFooterView(props: RunFooterViewProps) {
closePanel()
})
createEffect(() => {
if (route().type !== "queued-menu" || queuedPrompts().length > 0) return
closePanel()
})
createEffect(() => {
if (active().type === "prompt") {
return
@@ -355,6 +482,7 @@ export function RunFooterView(props: RunFooterViewProps) {
current.type !== "command" &&
current.type !== "model" &&
current.type !== "variant" &&
current.type !== "queued-menu" &&
current.type !== "subagent-menu"
) {
return
@@ -417,7 +545,6 @@ export function RunFooterView(props: RunFooterViewProps) {
<RunPromptBody
theme={theme}
placeholder={composer.placeholder}
bindings={composer.bindings}
onSubmit={composer.onSubmit}
onKeyDown={composer.onKeyDown}
onContentChange={composer.onContentChange}
@@ -434,16 +561,32 @@ export function RunFooterView(props: RunFooterViewProps) {
onRows={setSubagentMenuRows}
/>
</Match>
<Match when={selectingQueued()}>
<RunQueuedPromptSelectBody
theme={theme}
prompts={queuedPrompts}
onClose={closePanel}
onDelete={(item) => void props.onQueuedRemove(item.messageID)}
onEdit={async (item) => {
if (!(await props.onQueuedRemove(item.messageID))) return
closePanel()
queueMicrotask(() => composer.replacePrompt(item.prompt))
}}
onRows={setSubagentMenuRows}
/>
</Match>
<Match when={commanding()}>
<RunCommandMenuBody
theme={theme}
commands={props.commands}
subagents={tabs}
queued={queuedPrompts}
variants={props.variants}
keybinds={props.keybinds}
variantCycle={variantCycle()}
onClose={closePanel}
onModel={openModel}
onSubagent={openSubagentMenu}
onQueued={openQueuedMenu}
onVariant={openVariant}
onVariantCycle={() => {
props.onCycle()
@@ -529,16 +672,33 @@ export function RunFooterView(props: RunFooterViewProps) {
{shell() ? "Shell" : props.agent}
</text>
<Show when={!shell()}>
<text
id="run-direct-footer-model"
fg={theme().text}
wrapMode="none"
truncate
flexGrow={1}
flexShrink={1}
>
{props.state().model}
</text>
<box id="run-direct-footer-model" flexDirection="row" gap={1} flexGrow={1} flexShrink={1}>
<text fg={theme().muted} wrapMode="none" flexShrink={0}>
·
</text>
<text fg={theme().text} wrapMode="none" truncate flexShrink={1}>
{model().model}
</text>
<Show when={model().provider}>
{(provider) => (
<text fg={theme().muted} wrapMode="none" truncate flexShrink={1}>
{provider()}
</text>
)}
</Show>
<Show when={props.currentVariant()}>
{(variant) => (
<>
<text fg={theme().muted} wrapMode="none" flexShrink={0}>
·
</text>
<text wrapMode="none" truncate flexShrink={1}>
<span style={{ fg: theme().warning, bold: true }}>{variant()}</span>
</text>
</>
)}
</Show>
</box>
</Show>
</box>
</Show>
@@ -611,7 +771,9 @@ export function RunFooterView(props: RunFooterViewProps) {
gap={1}
flexShrink={0}
>
<Show when={busy() || exiting() || duration().length > 0 || subagentIndicator()}>
<Show
when={busy() || exiting() || duration().length > 0 || queuedIndicator() || subagentIndicator()}
>
<box id="run-direct-footer-hint-left" flexDirection="row" gap={1} flexShrink={0} marginLeft={1}>
<Show when={exiting()}>
<text id="run-direct-footer-hint-exit" fg={theme().highlight} wrapMode="none" truncate>
@@ -656,13 +818,25 @@ export function RunFooterView(props: RunFooterViewProps) {
<Show when={subagentIndicator()}>
{(info) => (
<text id="run-direct-footer-subagents-label" fg={theme().text} wrapMode="none" truncate>
<Show when={busy() || exiting() || duration().length > 0}>
<span style={{ fg: theme().muted }}>· </span>
</Show>
{info().count} <span style={{ fg: theme().muted }}>{info().label}</span>
<span style={{ fg: theme().muted }}> · </span>
<span style={{ fg: theme().highlight }}></span>
<span style={{ fg: theme().muted }}> to view</span>
<span style={{ fg: theme().highlight }}> </span>
{info().count} <span style={{ fg: theme().muted }}>{info().label} </span>
<span style={{ fg: theme().highlight }}>{subagentShortcut() || "leader+down"}</span>
</text>
)}
</Show>
<Show when={foregroundSubagents() && backgroundShortcut()}>
<text id="run-direct-footer-background-label" fg={theme().text} wrapMode="none" truncate>
<span style={{ fg: theme().highlight }}> </span>
<span style={{ fg: theme().highlight }}>{backgroundShortcut()}</span>{" "}
<span style={{ fg: theme().muted }}>background</span>
</text>
</Show>
<Show when={queuedIndicator()}>
{(info) => (
<text id="run-direct-footer-queued-label" fg={theme().text} wrapMode="none" truncate>
<span style={{ fg: theme().warning }}> </span>
{info().count} <span style={{ fg: theme().muted }}>queued </span>
<span style={{ fg: theme().highlight }}>{queuedShortcut() || "leader+q"}</span>
</text>
)}
</Show>
@@ -682,9 +856,9 @@ export function RunFooterView(props: RunFooterViewProps) {
when={shell()}
fallback={
<>
<Show when={queue() > 0}>
<Show when={additionalQueue() > 0}>
<text id="run-direct-footer-queue" fg={theme().muted} wrapMode="none" truncate>
{queue()} queued
{additionalQueue()} queued
</text>
</Show>
<Show when={usage().length > 0}>

Some files were not shown because too many files have changed in this diff Show More