Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ They are distinct from Cline SDK plugin runtime hooks such as `beforeRun`,
- `PreToolUse` for active tools like `Read`, `Grep`, `Glob`, `FetchUrl`, `WebSearch`, `Execute`, `Task`, `Edit`, and `Create` emits `to_in_progress`
- `PreToolUse` for `AskUser` and `Stop` emit `to_review`
- `PostToolUse` for `AskUser` and `UserPromptSubmit` emit `to_in_progress`
- Kimi Code
- Kanban launches with a generated `--config-file` that preserves the user's default TOML config and appends Kanban hook entries
- `UserPromptSubmit` and `PreToolUse` emit `to_in_progress`
- `Stop`, `StopFailure`, and approval/attention notifications emit `to_review`
- the initial task prompt is bracketed-pasted into the interactive TUI after the first terminal output

Important behavior details:

Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Kanban currently supports three runtime modes.
| Runtime mode | Used for | Scope | Backing implementation | Why it exists |
| --- | --- | --- | --- | --- |
| Native Cline chat | Cline | task-scoped, plus a project-scoped sidebar surface | Cline SDK session host | Cline exposes richer chat semantics, provider settings, OAuth, and persisted session history |
| CLI-backed task terminal | Claude Code, Codex, Gemini, OpenCode, Droid, and similar agents | task-scoped | PTY-backed process runtime | these agents are command-driven CLIs and already fit the terminal model well |
| CLI-backed task terminal | Claude Code, Codex, Gemini, OpenCode, Droid, Kimi Code, and similar agents | task-scoped | PTY-backed process runtime | these agents are command-driven CLIs and already fit the terminal model well |
| Workspace shell terminal | the bottom shell panel | workspace-scoped | PTY-backed shell process | this is for manual commands in the repo, not task execution |

The crucial point is that Cline is not just "another agent command". It is a native runtime path. Treating it like a terminal process would throw away useful structure that the SDK already gives us.
Expand Down Expand Up @@ -201,7 +201,7 @@ The `src/terminal/` area owns everything process-oriented:
- translating process lifecycle into Kanban runtime summaries
- handling the workspace shell terminal

This is the path for Claude Code, Codex, Gemini, OpenCode, Droid, and any other command-driven agent.
This is the path for Claude Code, Codex, Gemini, OpenCode, Droid, Kimi Code, and any other command-driven agent.

### Native Cline integration

Expand Down Expand Up @@ -391,7 +391,7 @@ When you are making a change, this table is often more useful than a file list.

| If you are changing... | Think about this first | Common mistake to avoid |
| --- | --- | --- |
| task startup for Claude Code, Codex, Gemini, OpenCode, or Droid | the PTY runtime and agent launch path | accidentally adding special logic to the Cline path |
| task startup for Claude Code, Codex, Gemini, OpenCode, Droid, or Kimi Code | the PTY runtime and agent launch path | accidentally adding special logic to the Cline path |
| Cline provider settings, models, or OAuth | the Cline provider service and SDK provider boundary | storing secrets in Kanban config or duplicating OAuth policy |
| Cline message rendering or send/cancel behavior | the shared Cline hooks and task-session service | making detail view and sidebar behave differently |
| live board updates | the runtime state hub and browser stream consumers | falling back to polling or duplicating summary logic |
Expand Down
7 changes: 5 additions & 2 deletions src/commands/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,7 +1125,10 @@ export function registerTaskCommand(program: Command): void {
.option("--start-in-plan-mode [value]", "Set plan mode (true|false). Flag-only implies true.")
.option("--auto-review-enabled [value]", "Enable auto-review behavior (true|false). Flag-only implies true.")
.option("--auto-review-mode <mode>", "Auto-review mode: commit | pr.", parseAutoReviewMode)
.option("--agent-id <id>", "Agent override: cline | claude | codex | droid | gemini | opencode | default.")
.option(
"--agent-id <id>",
"Agent override: cline | claude | codex | droid | kiro | kimi | gemini | opencode | default.",
)
.option(
"--cline-provider <id>",
'Cline provider override (e.g. anthropic, openai, cline). Use "default" for workspace default.',
Expand Down Expand Up @@ -1187,7 +1190,7 @@ export function registerTaskCommand(program: Command): void {
.option("--auto-review-mode <mode>", "Auto-review mode: commit | pr.", parseAutoReviewMode)
.option(
"--agent-id <id>",
'Agent override: cline | claude | codex | droid | gemini | opencode. Use "default" to clear.',
'Agent override: cline | claude | codex | droid | kiro | kimi | gemini | opencode. Use "default" to clear.',
)
.option(
"--cline-provider <id>",
Expand Down
3 changes: 2 additions & 1 deletion src/config/runtime-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const PROJECT_CONFIG_PARENT_DIR = ".cline";
const PROJECT_CONFIG_DIR = "kanban";
const PROJECT_CONFIG_FILENAME = "config.json";
const DEFAULT_AGENT_ID: RuntimeAgentId = "cline";
const AUTO_SELECT_AGENT_PRIORITY: readonly RuntimeAgentId[] = ["claude", "codex", "droid", "kiro"];
const AUTO_SELECT_AGENT_PRIORITY: readonly RuntimeAgentId[] = ["claude", "codex", "droid", "kiro", "kimi"];
const DEFAULT_AGENT_AUTONOMOUS_MODE_ENABLED = true;
const DEFAULT_READY_FOR_REVIEW_NOTIFICATIONS_ENABLED = true;
const DEFAULT_COMMIT_PROMPT_TEMPLATE = `You are in a worktree on a detached HEAD. When you are finished with the task, commit the working changes onto {{base_ref}}.
Expand Down Expand Up @@ -124,6 +124,7 @@ function normalizeAgentId(agentId: RuntimeAgentId | string | null | undefined):
agentId === "opencode" ||
agentId === "droid" ||
agentId === "kiro" ||
agentId === "kimi" ||
agentId === "cline") &&
isRuntimeAgentLaunchSupported(agentId)
) {
Expand Down
9 changes: 9 additions & 0 deletions src/core/agent-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export const RUNTIME_AGENT_CATALOG: RuntimeAgentCatalogEntry[] = [
autonomousArgs: ["--trust-all-tools"],
installUrl: "https://kiro.dev",
},
{
id: "kimi",
label: "Kimi Code",
binary: "kimi",
baseArgs: [],
autonomousArgs: ["--yolo"],
installUrl: "https://moonshotai.github.io/kimi-code/en/guides/getting-started",
},
{
id: "gemini",
label: "Gemini CLI",
Expand All @@ -76,6 +84,7 @@ export const RUNTIME_LAUNCH_SUPPORTED_AGENT_IDS: readonly RuntimeAgentId[] = [
"codex",
"droid",
"kiro",
"kimi",
// "opencode",
// "gemini",
];
Expand Down
2 changes: 1 addition & 1 deletion src/core/api-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const runtimeSlashCommandsResponseSchema = z.object({
});
export type RuntimeSlashCommandsResponse = z.infer<typeof runtimeSlashCommandsResponseSchema>;

export const runtimeAgentIdSchema = z.enum(["claude", "codex", "gemini", "opencode", "droid", "kiro", "cline"]);
export const runtimeAgentIdSchema = z.enum(["claude", "codex", "gemini", "opencode", "droid", "kiro", "kimi", "cline"]);
export type RuntimeAgentId = z.infer<typeof runtimeAgentIdSchema>;

const runtimeBoardColumnIdEnum = z.enum(["backlog", "in_progress", "review", "trash"]);
Expand Down
3 changes: 3 additions & 0 deletions src/prompts/append-system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const APPEND_PROMPT_AGENT_IDS: readonly RuntimeAgentId[] = [
"kiro",
"gemini",
"opencode",
"kimi",
];

function isRuntimeAgentId(value: string): value is RuntimeAgentId {
Expand Down Expand Up @@ -66,6 +67,8 @@ function renderLinearSetupGuidanceForAgent(agentId: RuntimeAgentId | null): stri
return "- If Linear MCP is not available in the current agent (Droid), suggest running: `droid mcp add linear https://mcp.linear.app/mcp --type http`";
case "kiro":
return "- If Linear MCP is not available in the current agent (Kiro CLI), suggest running: `kiro-cli mcp add --name linear --url https://mcp.linear.app/mcp --scope global`";
case "kimi":
return "- If Linear MCP is not available in the current agent (Kimi Code), suggest checking Kimi Code MCP setup and adding the Linear MCP server from `https://linear.app/docs/mcp`.";
default:
return "- If Linear MCP is not available, provide setup instructions for the active agent only, then continue once OAuth is complete.";
}
Expand Down
63 changes: 63 additions & 0 deletions src/terminal/agent-session-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { resolveHomeAgentAppendSystemPrompt } from "../prompts/append-system-pro
import { getRuntimeHomePath } from "../state/workspace-state";
import { configureCodexHooks, hasCodexConfigOverride } from "./codex-hook-config";
import { createHookRuntimeEnv } from "./hook-runtime-context";
import { ensureKimiKanbanAgentFile, getKimiKanbanAgentFilePath } from "./kimi-agent-config";
import { ensureKimiKanbanConfig, getKimiKanbanConfigPath } from "./kimi-config";
import {
getOpenCodeAuthPathCandidates,
getOpenCodeConfigPathCandidates,
Expand Down Expand Up @@ -127,6 +129,10 @@ function hasCliOption(args: string[], optionName: string): boolean {
return false;
}

function hasAnyCliOption(args: string[], optionNames: readonly string[]): boolean {
return optionNames.some((optionName) => hasCliOption(args, optionName));
}

function getClineHookScriptPath(
hooksDir: string,
hookName: "Notification" | "TaskComplete" | "UserPromptSubmit" | "PreToolUse" | "PostToolUse",
Expand Down Expand Up @@ -1370,6 +1376,62 @@ const kiroAdapter: AgentSessionAdapter = {
},
};

const kimiAdapter: AgentSessionAdapter = {
async prepare(input) {
const args = [...input.args];
const env: Record<string, string | undefined> = {};
const appendedSystemPrompt = resolveHomeAgentAppendSystemPrompt(input.taskId);

if (!hasAnyCliOption(args, ["--continue", "-C", "--session", "-S", "--resume", "-r"]) && input.resumeFromTrash) {
args.push("--continue");
}

if (
input.autonomousModeEnabled &&
!input.resumeFromTrash &&
!hasAnyCliOption(args, ["--yolo", "-y", "--yes", "--auto-approve", "--afk"])
) {
args.push("--yolo");
}

if (input.startInPlanMode && !hasCliOption(args, "--plan")) {
args.push("--plan");
}

if (appendedSystemPrompt && !hasAnyCliOption(args, ["--agent", "--agent-file"])) {
const agentFilePath = await ensureKimiKanbanAgentFile({
additionalSystemPrompt: appendedSystemPrompt,
agentFilePath: getKimiKanbanAgentFilePath(getRuntimeHomePath()),
});
args.push("--agent-file", agentFilePath);
}

const hooks = resolveHookContext(input);
if (hooks && !hasAnyCliOption(args, ["--config", "--config-file"])) {
const configPath = await ensureKimiKanbanConfig({
buildHookCommand: (event, metadata) => buildHookCommand(event, metadata),
configPath: getKimiKanbanConfigPath(getRuntimeHomePath()),
env: input.env,
});
args.push("--config-file", configPath);
Object.assign(
env,
createHookRuntimeEnv({
taskId: hooks.taskId,
workspaceId: hooks.workspaceId,
}),
);
}

const trimmedPrompt = input.prompt.trim();
return {
args,
env,
deferredStartupInput: trimmedPrompt ? toBracketedPasteSubmission(trimmedPrompt) : undefined,
};
},
};

const clineAdapter: AgentSessionAdapter = {
async prepare(input) {
const args = [...input.args];
Expand Down Expand Up @@ -1434,6 +1496,7 @@ const ADAPTERS: Record<RuntimeAgentId, AgentSessionAdapter> = {
opencode: opencodeAdapter,
droid: droidAdapter,
kiro: kiroAdapter,
kimi: kimiAdapter,
cline: clineAdapter,
};

Expand Down
37 changes: 37 additions & 0 deletions src/terminal/kimi-agent-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { join } from "node:path";

import { lockedFileSystem } from "../fs/locked-file-system";

export interface EnsureKimiKanbanAgentFileInput {
additionalSystemPrompt: string;
agentFilePath: string;
}

export function getKimiKanbanAgentFilePath(runtimeHomePath: string): string {
return join(runtimeHomePath, "hooks", "kimi", "agent.yaml");
}

export function buildKimiKanbanAgentFileContent(additionalSystemPrompt: string): string {
return `${JSON.stringify(
{
version: 1,
agent: {
extend: "default",
name: "kanban",
system_prompt_args: {
ROLE_ADDITIONAL: additionalSystemPrompt,
},
},
},
null,
2,
)}\n`;
Comment thread
moonixt marked this conversation as resolved.
}

export async function ensureKimiKanbanAgentFile(input: EnsureKimiKanbanAgentFileInput): Promise<string> {
await lockedFileSystem.writeTextFileAtomic(
input.agentFilePath,
buildKimiKanbanAgentFileContent(input.additionalSystemPrompt),
);
return input.agentFilePath;
}
Loading