From e773cc90fc510390dafc0b7d24e9b423adffa91b Mon Sep 17 00:00:00 2001
From: Davide Gallitelli
Date: Mon, 23 Mar 2026 04:24:41 +0000
Subject: [PATCH 1/5] feat: add Kiro (AWS Agentic IDE) as a 5th provider
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds Kiro — AWS's agentic IDE built on Claude — as a supported provider
in CloudUI, following the same patterns as Codex, Gemini, and Cursor CLI.
## What's added
**Backend (server)**
- `server/kiro-cli.js` — spawns `kiro` binary, streams NDJSON output via
WebSocket with a proper line-buffer to handle partial TCP chunks
- `server/providers/kiro/adapter.js` — normalises Kiro CLI events and
session history into NormalizedMessage format
- `server/routes/kiro.js` — DELETE /api/kiro/sessions/:sessionId endpoint
- `server/providers/registry.js` — registers kiroAdapter
- `server/providers/types.js` — adds 'kiro' to SessionProvider typedef
- `server/routes/agent.js` — kiro provider validation + dispatch branch
- `server/routes/messages.js` — JSDoc updated to include 'kiro'
- `server/projects.js` — getKiroSessions() + getKiroCliSessionMessages(),
wired into both project discovery loops; kiroSessions: [] in all shapes
- `server/index.js` — mounts kiro router, adds kiro to VALID_PROVIDERS,
watch paths, WS dispatch, abort/status/active-sessions handlers
- `shared/modelConstants.js` — KIRO_MODELS constant (Claude models)
**Frontend**
- `src/components/chat/hooks/useChatComposerState.ts` — kiro-command WS
dispatch branch; kiroModel threaded through
- `src/components/chat/hooks/useChatProviderState.ts` — kiroModel state
with localStorage persistence
- `src/components/chat/view/ChatInterface.tsx` — passes kiroModel down
- `src/components/chat/view/subcomponents/ChatMessagesPane.tsx` — kiroModel prop chain
- `src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx`
— Kiro card with KIRO_MODELS in the provider selection grid
- `src/components/llm-logo-provider/KiroLogo.tsx` — placeholder "K" badge
(TODO: replace with official Kiro SVG)
- `src/components/llm-logo-provider/SessionProviderLogo.tsx` — kiro branch
- `src/components/provider-auth/types.ts` — 'kiro' in CliProvider
- `src/components/settings/types/types.ts` — 'kiro' in AgentProvider
- `src/components/settings/view/.../AgentSelectorSection.tsx` — 'kiro'
in AGENT_PROVIDERS and AGENT_NAMES
- `src/components/onboarding/view/utils.ts` — 'kiro' in cliProviders and
createInitialProviderStatuses
- `src/components/sidebar/types/types.ts` — isKiroSession flag
- `src/components/sidebar/utils/utils.ts` — kiro session handling
- `src/hooks/useProjectsState.ts` — kiroSessions change detection
- `src/types/app.ts` — 'kiro' in SessionProvider union, kiroSessions field
## Known TODOs (needs Kiro CLI investigation)
Several parts are stubs pending confirmation of actual Kiro CLI behaviour:
- CLI argument flags (--prompt, --resume, --model, --output-format)
- NDJSON event schema (field names for message/tool_use/tool_result events)
- Session storage path (assumed ~/.kiro/sessions/)
See https://github.com/siteboon/claudecodeui/issues/574 for investigation plan.
Resolves #574
Co-Authored-By: Claude Sonnet 4.6
---
server/index.js | 36 +-
server/kiro-cli.js | 327 ++++++++++++++++++
server/projects.js | 160 ++++++++-
server/providers/kiro/adapter.js | 204 +++++++++++
server/providers/registry.js | 4 +-
server/providers/types.js | 2 +-
server/routes/agent.js | 21 +-
server/routes/kiro.js | 24 ++
server/routes/messages.js | 2 +-
shared/modelConstants.js | 21 ++
.../chat/hooks/useChatComposerState.ts | 26 +-
.../chat/hooks/useChatProviderState.ts | 11 +-
src/components/chat/view/ChatInterface.tsx | 5 +
.../view/subcomponents/ChatMessagesPane.tsx | 6 +
.../ProviderSelectionEmptyState.tsx | 23 ++
src/components/llm-logo-provider/KiroLogo.tsx | 16 +
.../llm-logo-provider/SessionProviderLogo.tsx | 5 +
src/components/onboarding/view/utils.ts | 3 +-
src/components/provider-auth/types.ts | 2 +-
src/components/settings/types/types.ts | 2 +-
.../sections/AgentSelectorSection.tsx | 3 +-
src/components/sidebar/types/types.ts | 1 +
src/components/sidebar/utils/utils.ts | 12 +-
src/hooks/useProjectsState.ts | 19 +-
src/types/app.ts | 3 +-
25 files changed, 916 insertions(+), 22 deletions(-)
create mode 100644 server/kiro-cli.js
create mode 100644 server/providers/kiro/adapter.js
create mode 100644 server/routes/kiro.js
create mode 100644 src/components/llm-logo-provider/KiroLogo.tsx
diff --git a/server/index.js b/server/index.js
index 5318fb359..307925788 100755
--- a/server/index.js
+++ b/server/index.js
@@ -49,6 +49,7 @@ import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getAct
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
+import { spawnKiro, abortKiroSession, isKiroSessionActive, getActiveKiroSessions } from './kiro-cli.js';
import sessionManager from './sessionManager.js';
import gitRoutes from './routes/git.js';
import authRoutes from './routes/auth.js';
@@ -64,6 +65,7 @@ import cliAuthRoutes from './routes/cli-auth.js';
import userRoutes from './routes/user.js';
import codexRoutes from './routes/codex.js';
import geminiRoutes from './routes/gemini.js';
+import kiroRoutes from './routes/kiro.js';
import pluginsRoutes from './routes/plugins.js';
import messagesRoutes from './routes/messages.js';
import { createNormalizedMessage } from './providers/types.js';
@@ -74,7 +76,7 @@ import { validateApiKey, authenticateToken, authenticateWebSocket } from './midd
import { IS_PLATFORM } from './constants/config.js';
import { getConnectableHost } from '../shared/networkHosts.js';
-const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
+const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'kiro'];
// File system watchers for provider project/session folders
const PROVIDER_WATCH_PATHS = [
@@ -82,7 +84,9 @@ const PROVIDER_WATCH_PATHS = [
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
- { provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
+ { provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') },
+ // TODO: verify actual Kiro session storage path (~/.kiro/sessions/ is best guess)
+ { provider: 'kiro', rootPath: path.join(os.homedir(), '.kiro', 'sessions') }
];
const WATCHER_IGNORED_PATTERNS = [
'**/node_modules/**',
@@ -395,6 +399,9 @@ app.use('/api/codex', authenticateToken, codexRoutes);
// Gemini API Routes (protected)
app.use('/api/gemini', authenticateToken, geminiRoutes);
+// Kiro API Routes (protected)
+app.use('/api/kiro', authenticateToken, kiroRoutes);
+
// Plugins API Routes (protected)
app.use('/api/plugins', authenticateToken, pluginsRoutes);
@@ -1513,6 +1520,12 @@ function handleChatConnection(ws, request) {
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
console.log('🤖 Model:', data.options?.model || 'default');
await spawnGemini(data.command, data.options, writer);
+ } else if (data.type === 'kiro-command') {
+ console.log('[DEBUG] Kiro message:', data.command || '[Continue/Resume]');
+ console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
+ console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
+ console.log('🤖 Model:', data.options?.model || 'default');
+ await spawnKiro(data.command, data.options, writer);
} else if (data.type === 'cursor-resume') {
// Backward compatibility: treat as cursor-command with resume and no prompt
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
@@ -1532,6 +1545,8 @@ function handleChatConnection(ws, request) {
success = abortCodexSession(data.sessionId);
} else if (provider === 'gemini') {
success = abortGeminiSession(data.sessionId);
+ } else if (provider === 'kiro') {
+ success = abortKiroSession(data.sessionId);
} else {
// Use Claude Agents SDK
success = await abortClaudeSDKSession(data.sessionId);
@@ -1566,6 +1581,8 @@ function handleChatConnection(ws, request) {
isActive = isCodexSessionActive(sessionId);
} else if (provider === 'gemini') {
isActive = isGeminiSessionActive(sessionId);
+ } else if (provider === 'kiro') {
+ isActive = isKiroSessionActive(sessionId);
} else {
// Use Claude Agents SDK
isActive = isClaudeSDKSessionActive(sessionId);
@@ -1599,7 +1616,8 @@ function handleChatConnection(ws, request) {
claude: getActiveClaudeSDKSessions(),
cursor: getActiveCursorSessions(),
codex: getActiveCodexSessions(),
- gemini: getActiveGeminiSessions()
+ gemini: getActiveGeminiSessions(),
+ kiro: getActiveKiroSessions()
};
writer.send({
type: 'active-sessions',
@@ -2249,6 +2267,18 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
});
}
+ // Handle Kiro sessions - token usage tracking not yet available
+ // TODO: implement Kiro token usage tracking once CLI format is known
+ if (provider === 'kiro') {
+ return res.json({
+ used: 0,
+ total: 0,
+ breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
+ unsupported: true,
+ message: 'Token usage tracking not available for Kiro sessions'
+ });
+ }
+
// Handle Codex sessions
if (provider === 'codex') {
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
diff --git a/server/kiro-cli.js b/server/kiro-cli.js
new file mode 100644
index 000000000..1f3606a19
--- /dev/null
+++ b/server/kiro-cli.js
@@ -0,0 +1,327 @@
+import { spawn } from 'child_process';
+import crossSpawn from 'cross-spawn';
+
+// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
+const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
+
+import os from 'os';
+import sessionManager from './sessionManager.js';
+import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
+import { createNormalizedMessage } from './providers/types.js';
+
+let activeKiroProcesses = new Map(); // Track active processes by session ID
+
+async function spawnKiro(command, options = {}, ws) {
+ const { sessionId, projectPath, cwd, toolsSettings, permissionMode, sessionSummary } = options;
+ let capturedSessionId = sessionId; // Track session ID throughout the process
+ let sessionCreatedSent = false; // Track if we've already sent session-created event
+ let assistantBlocks = []; // Accumulate the full response blocks including tools
+
+ // Use tools settings passed from frontend, or defaults
+ const settings = toolsSettings || {
+ allowedTools: [],
+ disallowedTools: [],
+ skipPermissions: false
+ };
+
+ // Build Kiro CLI command arguments
+ // TODO: verify actual Kiro CLI argument format — modeled after Gemini CLI structure
+ const args = [];
+
+ // Add prompt if we have a command
+ if (command && command.trim()) {
+ // TODO: verify correct Kiro CLI flag for passing a prompt (--prompt, --message, or positional arg)
+ args.push('--prompt', command);
+ }
+
+ // If we have a sessionId, attempt to resume
+ if (sessionId) {
+ const session = sessionManager.getSession(sessionId);
+ if (session && session.cliSessionId) {
+ // TODO: verify correct Kiro CLI flag for resuming a session (--resume, --session, etc.)
+ args.push('--resume', session.cliSessionId);
+ }
+ }
+
+ // Use cwd (actual project directory) instead of projectPath (metadata directory)
+ // Clean the path by removing any non-printable characters
+ const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim();
+ const workingDir = cleanPath;
+
+ // TODO: verify if Kiro CLI has a model selection flag
+ if (options.model) {
+ args.push('--model', options.model);
+ }
+
+ // TODO: verify Kiro CLI output format flag (--output-format, --json, etc.)
+ // Assuming JSON line output similar to Gemini CLI
+ args.push('--output-format', 'stream-json');
+
+ // Handle permission/approval modes
+ // TODO: verify Kiro CLI permission mode flags
+ if (settings.skipPermissions || options.skipPermissions || permissionMode === 'yolo') {
+ args.push('--yolo');
+ }
+
+ // Try to find kiro in PATH first, then fall back to environment variable
+ const kiroPath = process.env.KIRO_PATH || 'kiro';
+ console.log('Spawning Kiro CLI:', kiroPath, args.join(' '));
+ console.log('Working directory:', workingDir);
+
+ let spawnCmd = kiroPath;
+ let spawnArgs = args;
+
+ // On non-Windows platforms, wrap the execution in a shell to avoid ENOEXEC
+ // which happens when the target is a script lacking a shebang.
+ if (os.platform() !== 'win32') {
+ spawnCmd = 'sh';
+ // Use exec to replace the shell process, ensuring signals hit kiro directly
+ spawnArgs = ['-c', 'exec "$0" "$@"', kiroPath, ...args];
+ }
+
+ return new Promise((resolve, reject) => {
+ const kiroProcess = spawnFunction(spawnCmd, spawnArgs, {
+ cwd: workingDir,
+ stdio: ['pipe', 'pipe', 'pipe'],
+ env: { ...process.env } // Inherit all environment variables
+ });
+ let terminalNotificationSent = false;
+ let terminalFailureReason = null;
+
+ const notifyTerminalState = ({ code = null, error = null } = {}) => {
+ if (terminalNotificationSent) {
+ return;
+ }
+
+ terminalNotificationSent = true;
+
+ const finalSessionId = capturedSessionId || sessionId || processKey;
+ if (code === 0 && !error) {
+ notifyRunStopped({
+ userId: ws?.userId || null,
+ provider: 'kiro',
+ sessionId: finalSessionId,
+ sessionName: sessionSummary,
+ stopReason: 'completed'
+ });
+ return;
+ }
+
+ notifyRunFailed({
+ userId: ws?.userId || null,
+ provider: 'kiro',
+ sessionId: finalSessionId,
+ sessionName: sessionSummary,
+ error: error || terminalFailureReason || `Kiro CLI exited with code ${code}`
+ });
+ };
+
+ // Store process reference for potential abort
+ const processKey = capturedSessionId || sessionId || Date.now().toString();
+ activeKiroProcesses.set(processKey, kiroProcess);
+
+ // Store sessionId on the process object for debugging
+ kiroProcess.sessionId = processKey;
+
+ // Close stdin to signal we're done sending input
+ kiroProcess.stdin.end();
+
+ // Add timeout handler
+ const timeoutMs = 120000; // 120 seconds for slower models
+ let timeout;
+
+ const startTimeout = () => {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(() => {
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
+ terminalFailureReason = `Kiro CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
+ ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'kiro' }));
+ try {
+ kiroProcess.kill('SIGTERM');
+ } catch (e) { }
+ }, timeoutMs);
+ };
+
+ startTimeout();
+
+ // Save user message to session when starting
+ if (command && capturedSessionId) {
+ sessionManager.addMessage(capturedSessionId, 'user', command);
+ }
+
+ // Handle stdout
+ // TODO: verify Kiro CLI output format — modeled after Gemini CLI (NDJSON/JSON lines)
+ // The current implementation assumes JSON-lines output. Adjust parseKiroLine() if format differs.
+ let lineBuffer = '';
+ kiroProcess.stdout.on('data', (data) => {
+ lineBuffer += data.toString();
+ startTimeout(); // Re-arm the timeout
+
+ // For new sessions, create a session ID FIRST
+ if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
+ capturedSessionId = `kiro_${Date.now()}`;
+ sessionCreatedSent = true;
+
+ // Create session in session manager
+ sessionManager.createSession(capturedSessionId, cwd || process.cwd());
+
+ // Save the user message now that we have a session ID
+ if (command) {
+ sessionManager.addMessage(capturedSessionId, 'user', command);
+ }
+
+ // Update process key with captured session ID
+ if (processKey !== capturedSessionId) {
+ activeKiroProcesses.delete(processKey);
+ activeKiroProcesses.set(capturedSessionId, kiroProcess);
+ }
+
+ ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
+
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'kiro' }));
+ }
+
+ // Split on newlines and keep any incomplete last fragment in the buffer.
+ // This handles partial JSON lines split across TCP chunks.
+ const lines = lineBuffer.split('\n');
+ lineBuffer = lines.pop(); // keep incomplete last line for next data event
+
+ // TODO: verify Kiro CLI output format and update this parsing logic accordingly.
+ // Currently treating each complete line as a potential JSON object (NDJSON), falling back to raw text.
+ for (const line of lines) {
+ if (!line.trim()) continue;
+ try {
+ const parsed = JSON.parse(line);
+ // TODO: map actual Kiro CLI JSON event types to NormalizedMessage kinds
+ // For now, extract text content from common fields
+ const content = parsed.content || parsed.text || parsed.message || parsed.output || '';
+ if (content) {
+ if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
+ assistantBlocks[assistantBlocks.length - 1].text += content;
+ } else {
+ assistantBlocks.push({ type: 'text', text: content });
+ }
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
+ ws.send(createNormalizedMessage({ kind: 'stream_delta', content, sessionId: socketSessionId, provider: 'kiro' }));
+ }
+ } catch {
+ // Not JSON — treat as raw text output
+ if (line.trim()) {
+ if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
+ assistantBlocks[assistantBlocks.length - 1].text += line;
+ } else {
+ assistantBlocks.push({ type: 'text', text: line });
+ }
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
+ ws.send(createNormalizedMessage({ kind: 'stream_delta', content: line, sessionId: socketSessionId, provider: 'kiro' }));
+ }
+ }
+ }
+ });
+
+ // Handle stderr
+ kiroProcess.stderr.on('data', (data) => {
+ const errorMsg = data.toString();
+
+ // Filter out common non-error messages
+ // TODO: add Kiro-specific stderr filters once CLI output is known
+ if (errorMsg.includes('[DEP0040]') ||
+ errorMsg.includes('DeprecationWarning') ||
+ errorMsg.includes('--trace-deprecation')) {
+ return;
+ }
+
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorMsg, sessionId: socketSessionId, provider: 'kiro' }));
+ });
+
+ // Handle process completion
+ kiroProcess.on('close', async (code) => {
+ clearTimeout(timeout);
+
+ // Clean up process reference
+ const finalSessionId = capturedSessionId || sessionId || processKey;
+ activeKiroProcesses.delete(finalSessionId);
+
+ // Save assistant response to session if we have one
+ if (finalSessionId && assistantBlocks.length > 0) {
+ sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
+ }
+
+ ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'kiro' }));
+
+ if (code === 0) {
+ notifyTerminalState({ code });
+ resolve();
+ } else {
+ notifyTerminalState({
+ code,
+ error: code === null ? 'Kiro CLI process was terminated or timed out' : null
+ });
+ reject(new Error(code === null ? 'Kiro CLI process was terminated or timed out' : `Kiro CLI exited with code ${code}`));
+ }
+ });
+
+ // Handle process errors
+ kiroProcess.on('error', (error) => {
+ // Clean up process reference on error
+ const finalSessionId = capturedSessionId || sessionId || processKey;
+ activeKiroProcesses.delete(finalSessionId);
+
+ const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
+ ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: errorSessionId, provider: 'kiro' }));
+ notifyTerminalState({ error });
+
+ reject(error);
+ });
+
+ });
+}
+
+function abortKiroSession(sessionId) {
+ let kiroProc = activeKiroProcesses.get(sessionId);
+ let processKey = sessionId;
+
+ if (!kiroProc) {
+ for (const [key, proc] of activeKiroProcesses.entries()) {
+ if (proc.sessionId === sessionId) {
+ kiroProc = proc;
+ processKey = key;
+ break;
+ }
+ }
+ }
+
+ if (kiroProc) {
+ try {
+ kiroProc.kill('SIGTERM');
+ setTimeout(() => {
+ if (activeKiroProcesses.has(processKey)) {
+ try {
+ kiroProc.kill('SIGKILL');
+ } catch (e) { }
+ }
+ }, 2000); // Wait 2 seconds before force kill
+
+ return true;
+ } catch (error) {
+ return false;
+ }
+ }
+ return false;
+}
+
+function isKiroSessionActive(sessionId) {
+ return activeKiroProcesses.has(sessionId);
+}
+
+function getActiveKiroSessions() {
+ return Array.from(activeKiroProcesses.keys());
+}
+
+export {
+ spawnKiro,
+ abortKiroSession,
+ isKiroSessionActive,
+ getActiveKiroSessions
+};
diff --git a/server/projects.js b/server/projects.js
index d8ccaeb7b..56b05d1c3 100755
--- a/server/projects.js
+++ b/server/projects.js
@@ -438,6 +438,7 @@ async function getProjects(progressCallback = null) {
isCustomName: !!customName,
sessions: [],
geminiSessions: [],
+ kiroSessions: [],
sessionMeta: {
hasMore: false,
total: 0
@@ -494,6 +495,16 @@ async function getProjects(progressCallback = null) {
}
applyCustomSessionNames(project.geminiSessions, 'gemini');
+ // Also fetch Kiro sessions for this project
+ // TODO: verify actual Kiro session storage path once CLI is available
+ try {
+ project.kiroSessions = await getKiroSessions(actualProjectDir);
+ } catch (e) {
+ console.warn(`Could not load Kiro sessions for project ${entry.name}:`, e.message);
+ project.kiroSessions = [];
+ }
+ applyCustomSessionNames(project.kiroSessions, 'kiro');
+
// Add TaskMaster detection
try {
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
@@ -562,6 +573,7 @@ async function getProjects(progressCallback = null) {
isManuallyAdded: true,
sessions: [],
geminiSessions: [],
+ kiroSessions: [],
sessionMeta: {
hasMore: false,
total: 0
@@ -599,6 +611,15 @@ async function getProjects(progressCallback = null) {
}
applyCustomSessionNames(project.geminiSessions, 'gemini');
+ // Try to fetch Kiro sessions for manual projects too
+ // TODO: verify actual Kiro session storage path once CLI is available
+ try {
+ project.kiroSessions = await getKiroSessions(actualProjectDir);
+ } catch (e) {
+ console.warn(`Could not load Kiro sessions for manual project ${projectName}:`, e.message);
+ }
+ applyCustomSessionNames(project.kiroSessions, 'kiro');
+
// Add TaskMaster detection for manual projects
try {
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
@@ -1266,7 +1287,8 @@ async function addProjectManually(projectPath, displayName = null) {
displayName: displayName || await generateDisplayName(projectName, absolutePath),
isManuallyAdded: true,
sessions: [],
- cursorSessions: []
+ cursorSessions: [],
+ kiroSessions: []
};
}
@@ -2538,6 +2560,140 @@ async function getGeminiCliSessionMessages(sessionId) {
return [];
}
+/**
+ * Discover Kiro sessions for a given project path.
+ *
+ * TODO: verify actual Kiro session storage path once Kiro CLI is available.
+ * Most likely candidates:
+ * - ~/.kiro/sessions/ (global sessions)
+ * - /.kiro/ (project-local specs/tasks that act like session context)
+ *
+ * Currently returns sessions from ~/.kiro/sessions/ if the directory exists,
+ * filtering by project path if a project root file is present.
+ */
+async function getKiroSessions(projectPath) {
+ // TODO: verify actual Kiro session storage path — this is a best-effort stub
+ const kiroSessionsDir = path.join(os.homedir(), '.kiro', 'sessions');
+ try {
+ await fs.access(kiroSessionsDir);
+ } catch {
+ return [];
+ }
+
+ const sessions = [];
+ let sessionFiles;
+ try {
+ sessionFiles = await fs.readdir(kiroSessionsDir);
+ } catch {
+ return [];
+ }
+
+ const normalizedProjectPath = normalizeComparablePath(projectPath);
+
+ for (const sessionFile of sessionFiles) {
+ if (!sessionFile.endsWith('.json') && !sessionFile.endsWith('.jsonl')) continue;
+ try {
+ const filePath = path.join(kiroSessionsDir, sessionFile);
+ const data = await fs.readFile(filePath, 'utf8');
+
+ // TODO: verify actual Kiro session file format — using best-guess field names
+ let session;
+ try {
+ session = JSON.parse(data);
+ } catch {
+ continue;
+ }
+
+ // If session has a projectPath field, filter by it
+ if (session.projectPath && normalizedProjectPath) {
+ if (normalizeComparablePath(session.projectPath) !== normalizedProjectPath) {
+ continue;
+ }
+ }
+
+ const sessionId = session.sessionId || session.id || sessionFile.replace(/\.(json|jsonl)$/, '');
+ const messages = session.messages || [];
+ const firstUserMsg = messages.find(m => m.role === 'user' || m.type === 'user');
+ let summary = 'Kiro Session';
+ if (firstUserMsg) {
+ const text = typeof firstUserMsg.content === 'string'
+ ? firstUserMsg.content
+ : Array.isArray(firstUserMsg.content)
+ ? firstUserMsg.content.filter(p => p.text).map(p => p.text).join(' ')
+ : '';
+ if (text) {
+ summary = text.length > 50 ? text.substring(0, 50) + '...' : text;
+ }
+ }
+
+ sessions.push({
+ id: sessionId,
+ summary,
+ messageCount: messages.length,
+ lastActivity: session.lastUpdated || session.updatedAt || session.startTime || null,
+ provider: 'kiro'
+ });
+ } catch {
+ continue;
+ }
+ }
+
+ return sessions.sort((a, b) =>
+ new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0)
+ );
+}
+
+/**
+ * Get messages for a specific Kiro CLI session by ID.
+ *
+ * TODO: verify actual Kiro session file format and storage path.
+ */
+async function getKiroCliSessionMessages(sessionId) {
+ // TODO: verify actual Kiro session storage path (~/.kiro/sessions/ or similar)
+ const kiroSessionsDir = path.join(os.homedir(), '.kiro', 'sessions');
+ let sessionFiles;
+ try {
+ sessionFiles = await fs.readdir(kiroSessionsDir);
+ } catch {
+ return [];
+ }
+
+ for (const sessionFile of sessionFiles) {
+ if (!sessionFile.endsWith('.json') && !sessionFile.endsWith('.jsonl')) continue;
+ try {
+ const filePath = path.join(kiroSessionsDir, sessionFile);
+ const data = await fs.readFile(filePath, 'utf8');
+ const session = JSON.parse(data);
+ const fileSessionId = session.sessionId || session.id || sessionFile.replace(/\.(json|jsonl)$/, '');
+ if (fileSessionId !== sessionId) continue;
+
+ // TODO: verify actual Kiro message format field names
+ return (session.messages || []).map(msg => {
+ const role = msg.role === 'user' ? 'user'
+ : (msg.role === 'assistant' || msg.type === 'assistant') ? 'assistant'
+ : msg.role || msg.type;
+
+ let content = '';
+ if (typeof msg.content === 'string') {
+ content = msg.content;
+ } else if (Array.isArray(msg.content)) {
+ content = msg.content.filter(p => p.text).map(p => p.text).join('\n');
+ }
+
+ return {
+ type: 'message',
+ message: { role, content },
+ timestamp: msg.timestamp || null
+ };
+ });
+ } catch {
+ continue;
+ }
+ }
+
+ return [];
+}
+
export {
getProjects,
getSessions,
@@ -2557,5 +2713,7 @@ export {
deleteCodexSession,
getGeminiCliSessions,
getGeminiCliSessionMessages,
+ getKiroSessions,
+ getKiroCliSessionMessages,
searchConversations
};
diff --git a/server/providers/kiro/adapter.js b/server/providers/kiro/adapter.js
new file mode 100644
index 000000000..aa54bce9c
--- /dev/null
+++ b/server/providers/kiro/adapter.js
@@ -0,0 +1,204 @@
+/**
+ * Kiro provider adapter.
+ *
+ * Normalizes Kiro CLI session history into NormalizedMessage format.
+ * Kiro is AWS's agentic IDE built on Claude (https://kiro.dev).
+ *
+ * TODO: verify actual Kiro CLI output format once CLI is available.
+ * Currently modeled after the Gemini/Codex adapter patterns with stubs
+ * for the unknown parts.
+ *
+ * @module adapters/kiro
+ */
+
+import sessionManager from '../../sessionManager.js';
+import { getKiroCliSessionMessages } from '../../projects.js';
+import { createNormalizedMessage, generateMessageId } from '../types.js';
+
+const PROVIDER = 'kiro';
+
+/**
+ * Normalize a realtime NDJSON event from Kiro CLI into NormalizedMessage(s).
+ *
+ * TODO: verify Kiro CLI output format — event type names and field names
+ * are inferred from similarity to Gemini CLI. Update once actual CLI output
+ * is confirmed.
+ *
+ * @param {object} raw - A parsed NDJSON event from Kiro CLI stdout
+ * @param {string} sessionId
+ * @returns {import('../types.js').NormalizedMessage[]}
+ */
+export function normalizeMessage(raw, sessionId) {
+ const ts = raw.timestamp || new Date().toISOString();
+ const baseId = raw.uuid || generateMessageId('kiro');
+
+ // TODO: verify actual Kiro CLI event type values
+ // Assuming similar structure to Gemini CLI for now
+
+ if (raw.type === 'message' && raw.role === 'assistant') {
+ const content = raw.content || raw.text || '';
+ const msgs = [];
+ if (content) {
+ msgs.push(createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_delta', content }));
+ }
+ // If not a delta, also send stream_end
+ if (raw.delta !== true) {
+ msgs.push(createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' }));
+ }
+ return msgs;
+ }
+
+ // TODO: verify Kiro CLI tool_use event field names
+ if (raw.type === 'tool_use') {
+ return [createNormalizedMessage({
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
+ kind: 'tool_use', toolName: raw.tool_name, toolInput: raw.parameters || raw.input || {},
+ toolId: raw.tool_id || baseId,
+ })];
+ }
+
+ // TODO: verify Kiro CLI tool_result event field names
+ if (raw.type === 'tool_result') {
+ return [createNormalizedMessage({
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
+ kind: 'tool_result', toolId: raw.tool_id || '',
+ content: raw.output === undefined ? '' : String(raw.output),
+ isError: raw.status === 'error',
+ })];
+ }
+
+ // TODO: verify Kiro CLI result/completion event type name and fields
+ if (raw.type === 'result' || raw.type === 'complete') {
+ const msgs = [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
+ if (raw.stats?.total_tokens) {
+ msgs.push(createNormalizedMessage({
+ sessionId, timestamp: ts, provider: PROVIDER,
+ kind: 'status', text: 'Complete', tokens: raw.stats.total_tokens, canInterrupt: false,
+ }));
+ }
+ return msgs;
+ }
+
+ if (raw.type === 'error') {
+ return [createNormalizedMessage({
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
+ kind: 'error', content: raw.error || raw.message || 'Unknown Kiro streaming error',
+ })];
+ }
+
+ return [];
+}
+
+/**
+ * @type {import('../types.js').ProviderAdapter}
+ */
+export const kiroAdapter = {
+ normalizeMessage,
+ /**
+ * Fetch session history for Kiro.
+ * First tries in-memory session manager, then falls back to CLI sessions on disk.
+ *
+ * TODO: verify actual Kiro session storage path once CLI is available.
+ */
+ async fetchHistory(sessionId, opts = {}) {
+ let rawMessages;
+ try {
+ rawMessages = sessionManager.getSessionMessages(sessionId);
+
+ // Fallback to Kiro CLI sessions on disk
+ // TODO: verify actual Kiro session storage path (~/.kiro/sessions/ or similar)
+ if (rawMessages.length === 0) {
+ rawMessages = await getKiroCliSessionMessages(sessionId);
+ }
+ } catch (error) {
+ console.warn(`[KiroAdapter] Failed to load session ${sessionId}:`, error.message);
+ return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
+ }
+
+ const normalized = [];
+ for (let i = 0; i < rawMessages.length; i++) {
+ const raw = rawMessages[i];
+ const ts = raw.timestamp || new Date().toISOString();
+ const baseId = raw.uuid || generateMessageId('kiro');
+
+ // sessionManager format: { type: 'message', message: { role, content }, timestamp }
+ const role = raw.message?.role || raw.role;
+ const content = raw.message?.content || raw.content;
+
+ if (!role || !content) continue;
+
+ const normalizedRole = (role === 'user') ? 'user' : 'assistant';
+
+ if (Array.isArray(content)) {
+ for (let partIdx = 0; partIdx < content.length; partIdx++) {
+ const part = content[partIdx];
+ if (part.type === 'text' && part.text) {
+ normalized.push(createNormalizedMessage({
+ id: `${baseId}_${partIdx}`,
+ sessionId,
+ timestamp: ts,
+ provider: PROVIDER,
+ kind: 'text',
+ role: normalizedRole,
+ content: part.text,
+ }));
+ } else if (part.type === 'tool_use') {
+ normalized.push(createNormalizedMessage({
+ id: `${baseId}_${partIdx}`,
+ sessionId,
+ timestamp: ts,
+ provider: PROVIDER,
+ kind: 'tool_use',
+ toolName: part.name,
+ toolInput: part.input,
+ toolId: part.id || generateMessageId('kiro_tool'),
+ }));
+ } else if (part.type === 'tool_result') {
+ normalized.push(createNormalizedMessage({
+ id: `${baseId}_${partIdx}`,
+ sessionId,
+ timestamp: ts,
+ provider: PROVIDER,
+ kind: 'tool_result',
+ toolId: part.tool_use_id || '',
+ content: part.content === undefined ? '' : String(part.content),
+ isError: Boolean(part.is_error),
+ }));
+ }
+ }
+ } else if (typeof content === 'string' && content.trim()) {
+ normalized.push(createNormalizedMessage({
+ id: baseId,
+ sessionId,
+ timestamp: ts,
+ provider: PROVIDER,
+ kind: 'text',
+ role: normalizedRole,
+ content,
+ }));
+ }
+ }
+
+ // Attach tool results to tool_use messages
+ const toolResultMap = new Map();
+ for (const msg of normalized) {
+ if (msg.kind === 'tool_result' && msg.toolId) {
+ toolResultMap.set(msg.toolId, msg);
+ }
+ }
+ for (const msg of normalized) {
+ if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
+ const tr = toolResultMap.get(msg.toolId);
+ msg.toolResult = { content: tr.content, isError: tr.isError };
+ }
+ }
+
+ return {
+ messages: normalized,
+ total: normalized.length,
+ hasMore: false,
+ offset: 0,
+ limit: null,
+ };
+ },
+};
diff --git a/server/providers/registry.js b/server/providers/registry.js
index 236c909ee..40686e6bf 100644
--- a/server/providers/registry.js
+++ b/server/providers/registry.js
@@ -11,6 +11,7 @@ import { claudeAdapter } from './claude/adapter.js';
import { cursorAdapter } from './cursor/adapter.js';
import { codexAdapter } from './codex/adapter.js';
import { geminiAdapter } from './gemini/adapter.js';
+import { kiroAdapter } from './kiro/adapter.js';
/**
* @typedef {import('./types.js').ProviderAdapter} ProviderAdapter
@@ -25,10 +26,11 @@ providers.set('claude', claudeAdapter);
providers.set('cursor', cursorAdapter);
providers.set('codex', codexAdapter);
providers.set('gemini', geminiAdapter);
+providers.set('kiro', kiroAdapter);
/**
* Get a provider adapter by name.
- * @param {string} name - Provider name (e.g., 'claude', 'cursor', 'codex', 'gemini')
+ * @param {string} name - Provider name (e.g., 'claude', 'cursor', 'codex', 'gemini', 'kiro')
* @returns {ProviderAdapter | undefined}
*/
export function getProvider(name) {
diff --git a/server/providers/types.js b/server/providers/types.js
index 5541525be..333495fb0 100644
--- a/server/providers/types.js
+++ b/server/providers/types.js
@@ -11,7 +11,7 @@
// ─── Session Provider ────────────────────────────────────────────────────────
/**
- * @typedef {'claude' | 'cursor' | 'codex' | 'gemini'} SessionProvider
+ * @typedef {'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro'} SessionProvider
*/
// ─── Message Kind ────────────────────────────────────────────────────────────
diff --git a/server/routes/agent.js b/server/routes/agent.js
index bf2d36de8..c7cfe1b42 100644
--- a/server/routes/agent.js
+++ b/server/routes/agent.js
@@ -10,8 +10,9 @@ import { queryClaudeSDK } from '../claude-sdk.js';
import { spawnCursor } from '../cursor-cli.js';
import { queryCodex } from '../openai-codex.js';
import { spawnGemini } from '../gemini-cli.js';
+import { spawnKiro } from '../kiro-cli.js';
import { Octokit } from '@octokit/rest';
-import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
+import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS, KIRO_MODELS } from '../../shared/modelConstants.js';
import { IS_PLATFORM } from '../constants/config.js';
const router = express.Router();
@@ -632,7 +633,7 @@ class ResponseCollector {
* - Source for auto-generated branch names (if createBranch=true and no branchName)
* - Fallback for PR title if no commits are made
*
- * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini'
+ * @param {string} provider - (Optional) AI provider to use. Options: 'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro'
* Default: 'claude'
*
* @param {boolean} stream - (Optional) Enable Server-Sent Events (SSE) streaming for real-time updates.
@@ -750,7 +751,7 @@ class ResponseCollector {
* Input Validations (400 Bad Request):
* - Either githubUrl OR projectPath must be provided (not neither)
* - message must be non-empty string
- * - provider must be 'claude', 'cursor', 'codex', or 'gemini'
+ * - provider must be 'claude', 'cursor', 'codex', 'gemini', or 'kiro'
* - createBranch/createPR requires githubUrl OR projectPath (not neither)
* - branchName must pass Git naming rules (if provided)
*
@@ -858,8 +859,8 @@ router.post('/', validateExternalApiKey, async (req, res) => {
return res.status(400).json({ error: 'message is required' });
}
- if (!['claude', 'cursor', 'codex', 'gemini'].includes(provider)) {
- return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", or "gemini"' });
+ if (!['claude', 'cursor', 'codex', 'gemini', 'kiro'].includes(provider)) {
+ return res.status(400).json({ error: 'provider must be "claude", "cursor", "codex", "gemini", or "kiro"' });
}
// Validate GitHub branch/PR creation requirements
@@ -984,6 +985,16 @@ router.post('/', validateExternalApiKey, async (req, res) => {
model: model,
skipPermissions: true // CLI mode bypasses permissions
}, writer);
+ } else if (provider === 'kiro') {
+ console.log('Starting Kiro CLI session');
+
+ await spawnKiro(message.trim(), {
+ projectPath: finalProjectPath,
+ cwd: finalProjectPath,
+ sessionId: null,
+ model: model || KIRO_MODELS.DEFAULT,
+ skipPermissions: true // CLI mode bypasses permissions
+ }, writer);
}
// Handle GitHub branch and PR creation after successful agent completion
diff --git a/server/routes/kiro.js b/server/routes/kiro.js
new file mode 100644
index 000000000..97b7e9d88
--- /dev/null
+++ b/server/routes/kiro.js
@@ -0,0 +1,24 @@
+import express from 'express';
+import sessionManager from '../sessionManager.js';
+import { sessionNamesDb } from '../database/db.js';
+
+const router = express.Router();
+
+router.delete('/sessions/:sessionId', async (req, res) => {
+ try {
+ const { sessionId } = req.params;
+
+ if (!sessionId || typeof sessionId !== 'string' || !/^[a-zA-Z0-9_.-]{1,100}$/.test(sessionId)) {
+ return res.status(400).json({ success: false, error: 'Invalid session ID format' });
+ }
+
+ await sessionManager.deleteSession(sessionId);
+ sessionNamesDb.deleteName(sessionId, 'kiro');
+ res.json({ success: true });
+ } catch (error) {
+ console.error(`Error deleting Kiro session ${req.params.sessionId}:`, error);
+ res.status(500).json({ success: false, error: error.message });
+ }
+});
+
+export default router;
diff --git a/server/routes/messages.js b/server/routes/messages.js
index 8eb14b37b..141161d58 100644
--- a/server/routes/messages.js
+++ b/server/routes/messages.js
@@ -20,7 +20,7 @@ const router = express.Router();
* Auth: authenticateToken applied at mount level in index.js
*
* Query params:
- * provider - 'claude' | 'cursor' | 'codex' | 'gemini' (default: 'claude')
+ * provider - 'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro' (default: 'claude')
* projectName - required for claude provider
* projectPath - required for cursor provider (absolute path used for cwdId hash)
* limit - page size (omit or null for all)
diff --git a/shared/modelConstants.js b/shared/modelConstants.js
index 514a17725..ac466bb6e 100644
--- a/shared/modelConstants.js
+++ b/shared/modelConstants.js
@@ -69,6 +69,27 @@ export const CODEX_MODELS = {
DEFAULT: "gpt-5.4",
};
+/**
+ * Kiro Models
+ *
+ * Kiro is AWS's agentic IDE built on Claude (https://kiro.dev).
+ * It uses Claude models under the hood via AWS infrastructure.
+ *
+ * TODO: verify exact model identifiers accepted by the Kiro CLI --model flag.
+ * Values below are reasonable defaults based on Kiro's Claude foundation.
+ */
+export const KIRO_MODELS = {
+ OPTIONS: [
+ { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
+ { value: "claude-opus-4-5", label: "Claude Opus 4.5" },
+ { value: "claude-sonnet-4", label: "Claude Sonnet 4" },
+ { value: "claude-opus-4", label: "Claude Opus 4" },
+ { value: "claude-haiku-4", label: "Claude Haiku 4" },
+ ],
+
+ DEFAULT: "claude-sonnet-4-5",
+};
+
/**
* Gemini Models
*/
diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts
index 6e84982d4..d93aa2820 100644
--- a/src/components/chat/hooks/useChatComposerState.ts
+++ b/src/components/chat/hooks/useChatComposerState.ts
@@ -40,6 +40,7 @@ interface UseChatComposerStateArgs {
claudeModel: string;
codexModel: string;
geminiModel: string;
+ kiroModel: string;
isLoading: boolean;
canAbortSession: boolean;
tokenBudget: Record | null;
@@ -112,6 +113,7 @@ export function useChatComposerState({
claudeModel,
codexModel,
geminiModel,
+ kiroModel,
isLoading,
canAbortSession,
tokenBudget,
@@ -281,7 +283,7 @@ export function useChatComposerState({
projectName: selectedProject.name,
sessionId: currentSessionId,
provider,
- model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : claudeModel,
+ model: provider === 'cursor' ? cursorModel : provider === 'codex' ? codexModel : provider === 'gemini' ? geminiModel : provider === 'kiro' ? kiroModel : claudeModel,
tokenUsage: tokenBudget,
};
@@ -333,6 +335,7 @@ export function useChatComposerState({
currentSessionId,
cursorModel,
geminiModel,
+ kiroModel,
handleBuiltInCommand,
handleCustomCommand,
input,
@@ -571,7 +574,9 @@ export function useChatComposerState({
? 'codex-settings'
: provider === 'gemini'
? 'gemini-settings'
- : 'claude-settings';
+ : provider === 'kiro'
+ ? 'kiro-settings'
+ : 'claude-settings';
const savedSettings = safeLocalStorage.getItem(settingsKey);
if (savedSettings) {
return JSON.parse(savedSettings);
@@ -638,6 +643,22 @@ export function useChatComposerState({
toolsSettings,
},
});
+ } else if (provider === 'kiro') {
+ sendMessage({
+ type: 'kiro-command',
+ command: messageContent,
+ sessionId: effectiveSessionId,
+ options: {
+ cwd: resolvedProjectPath,
+ projectPath: resolvedProjectPath,
+ sessionId: effectiveSessionId,
+ resume: Boolean(effectiveSessionId),
+ model: kiroModel,
+ sessionSummary,
+ permissionMode,
+ toolsSettings,
+ },
+ });
} else {
sendMessage({
type: 'claude-command',
@@ -680,6 +701,7 @@ export function useChatComposerState({
cursorModel,
executeCommand,
geminiModel,
+ kiroModel,
isLoading,
onSessionActive,
onSessionProcessing,
diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts
index 9d48ce3dd..402fb551e 100644
--- a/src/components/chat/hooks/useChatProviderState.ts
+++ b/src/components/chat/hooks/useChatProviderState.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { authenticatedFetch } from '../../../utils/api';
-import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS } from '../../../../shared/modelConstants';
+import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS, KIRO_MODELS } from '../../../../shared/modelConstants';
import type { PendingPermissionRequest, PermissionMode } from '../types/types';
import type { ProjectSession, SessionProvider } from '../../../types/app';
@@ -26,6 +26,9 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
const [geminiModel, setGeminiModel] = useState(() => {
return localStorage.getItem('gemini-model') || GEMINI_MODELS.DEFAULT;
});
+ const [kiroModel, setKiroModel] = useState(() => {
+ return localStorage.getItem('kiro-model') || KIRO_MODELS.DEFAULT;
+ });
const lastProviderRef = useRef(provider);
@@ -83,6 +86,10 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
});
}, [provider]);
+ useEffect(() => {
+ localStorage.setItem('kiro-model', kiroModel);
+ }, [kiroModel]);
+
const cyclePermissionMode = useCallback(() => {
const modes: PermissionMode[] =
provider === 'codex'
@@ -110,6 +117,8 @@ export function useChatProviderState({ selectedSession }: UseChatProviderStateAr
setCodexModel,
geminiModel,
setGeminiModel,
+ kiroModel,
+ setKiroModel,
permissionMode,
setPermissionMode,
pendingPermissionRequests,
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
index 9f9d0bc0b..3e242db60 100644
--- a/src/components/chat/view/ChatInterface.tsx
+++ b/src/components/chat/view/ChatInterface.tsx
@@ -71,6 +71,8 @@ function ChatInterface({
setCodexModel,
geminiModel,
setGeminiModel,
+ kiroModel,
+ setKiroModel,
permissionMode,
pendingPermissionRequests,
setPendingPermissionRequests,
@@ -181,6 +183,7 @@ function ChatInterface({
claudeModel,
codexModel,
geminiModel,
+ kiroModel,
isLoading,
canAbortSession,
tokenBudget,
@@ -314,6 +317,8 @@ function ChatInterface({
setCodexModel={setCodexModel}
geminiModel={geminiModel}
setGeminiModel={setGeminiModel}
+ kiroModel={kiroModel}
+ setKiroModel={setKiroModel}
tasksEnabled={tasksEnabled}
isTaskMasterInstalled={isTaskMasterInstalled}
onShowAllTasks={onShowAllTasks}
diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
index e9cb6dda4..bb1d4f5f9 100644
--- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
+++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx
@@ -27,6 +27,8 @@ interface ChatMessagesPaneProps {
setCodexModel: (model: string) => void;
geminiModel: string;
setGeminiModel: (model: string) => void;
+ kiroModel: string;
+ setKiroModel: (model: string) => void;
tasksEnabled: boolean;
isTaskMasterInstalled: boolean | null;
onShowAllTasks?: (() => void) | null;
@@ -73,6 +75,8 @@ export default function ChatMessagesPane({
setCodexModel,
geminiModel,
setGeminiModel,
+ kiroModel,
+ setKiroModel,
tasksEnabled,
isTaskMasterInstalled,
onShowAllTasks,
@@ -157,6 +161,8 @@ export default function ChatMessagesPane({
setCodexModel={setCodexModel}
geminiModel={geminiModel}
setGeminiModel={setGeminiModel}
+ kiroModel={kiroModel}
+ setKiroModel={setKiroModel}
tasksEnabled={tasksEnabled}
isTaskMasterInstalled={isTaskMasterInstalled}
onShowAllTasks={onShowAllTasks}
diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
index 792b12c74..0f00c847e 100644
--- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
+++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
@@ -7,6 +7,7 @@ import {
CURSOR_MODELS,
CODEX_MODELS,
GEMINI_MODELS,
+ KIRO_MODELS,
} from "../../../../../shared/modelConstants";
import type { ProjectSession, SessionProvider } from "../../../../types/app";
import { NextTaskBanner } from "../../../task-master";
@@ -25,6 +26,8 @@ type ProviderSelectionEmptyStateProps = {
setCodexModel: (model: string) => void;
geminiModel: string;
setGeminiModel: (model: string) => void;
+ kiroModel: string;
+ setKiroModel: (model: string) => void;
tasksEnabled: boolean;
isTaskMasterInstalled: boolean | null;
onShowAllTasks?: (() => void) | null;
@@ -73,12 +76,21 @@ const PROVIDERS: ProviderDef[] = [
ring: "ring-blue-500/15",
check: "bg-blue-500 text-white",
},
+ {
+ id: "kiro",
+ name: "Kiro",
+ infoKey: "providerSelection.providerInfo.kiro",
+ accent: "border-orange-500 dark:border-orange-400",
+ ring: "ring-orange-500/15",
+ check: "bg-orange-500 text-white",
+ },
];
function getModelConfig(p: SessionProvider) {
if (p === "claude") return CLAUDE_MODELS;
if (p === "codex") return CODEX_MODELS;
if (p === "gemini") return GEMINI_MODELS;
+ if (p === "kiro") return KIRO_MODELS;
return CURSOR_MODELS;
}
@@ -88,10 +100,12 @@ function getModelValue(
cu: string,
co: string,
g: string,
+ ki: string,
) {
if (p === "claude") return c;
if (p === "codex") return co;
if (p === "gemini") return g;
+ if (p === "kiro") return ki;
return cu;
}
@@ -109,6 +123,8 @@ export default function ProviderSelectionEmptyState({
setCodexModel,
geminiModel,
setGeminiModel,
+ kiroModel,
+ setKiroModel,
tasksEnabled,
isTaskMasterInstalled,
onShowAllTasks,
@@ -135,6 +151,9 @@ export default function ProviderSelectionEmptyState({
} else if (provider === "gemini") {
setGeminiModel(value);
localStorage.setItem("gemini-model", value);
+ } else if (provider === "kiro") {
+ setKiroModel(value);
+ localStorage.setItem("kiro-model", value);
} else {
setCursorModel(value);
localStorage.setItem("cursor-model", value);
@@ -148,6 +167,7 @@ export default function ProviderSelectionEmptyState({
cursorModel,
codexModel,
geminiModel,
+ kiroModel,
);
/* ── New session — provider picker ── */
@@ -251,6 +271,9 @@ export default function ProviderSelectionEmptyState({
gemini: t("providerSelection.readyPrompt.gemini", {
model: geminiModel,
}),
+ kiro: t("providerSelection.readyPrompt.kiro", {
+ model: kiroModel,
+ }),
}[provider]
}
diff --git a/src/components/llm-logo-provider/KiroLogo.tsx b/src/components/llm-logo-provider/KiroLogo.tsx
new file mode 100644
index 000000000..f0fbc3f22
--- /dev/null
+++ b/src/components/llm-logo-provider/KiroLogo.tsx
@@ -0,0 +1,16 @@
+const KiroLogo = ({className = 'w-5 h-5'}) => {
+ // TODO: replace with official Kiro icon once available at /icons/kiro-icon.svg
+ // Kiro is AWS's agentic IDE built on Claude (https://kiro.dev)
+ // For now, render a simple "K" text badge as a fallback
+ return (
+
+ K
+
+ );
+};
+
+export default KiroLogo;
diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx
index 1eaef5239..ed916e76e 100644
--- a/src/components/llm-logo-provider/SessionProviderLogo.tsx
+++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx
@@ -3,6 +3,7 @@ import ClaudeLogo from './ClaudeLogo';
import CodexLogo from './CodexLogo';
import CursorLogo from './CursorLogo';
import GeminiLogo from './GeminiLogo';
+import KiroLogo from './KiroLogo';
type SessionProviderLogoProps = {
provider?: SessionProvider | string | null;
@@ -25,5 +26,9 @@ export default function SessionProviderLogo({
return ;
}
+ if (provider === 'kiro') {
+ return ;
+ }
+
return ;
}
diff --git a/src/components/onboarding/view/utils.ts b/src/components/onboarding/view/utils.ts
index adb3c4bcc..5376ddf3a 100644
--- a/src/components/onboarding/view/utils.ts
+++ b/src/components/onboarding/view/utils.ts
@@ -1,7 +1,7 @@
import { IS_PLATFORM } from '../../../constants/config';
import type { CliProvider, ProviderStatusMap } from './types';
-export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
+export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'kiro'];
export const gitEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -17,6 +17,7 @@ export const createInitialProviderStatuses = (): ProviderStatusMap => ({
cursor: { authenticated: false, email: null, loading: true, error: null },
codex: { authenticated: false, email: null, loading: true, error: null },
gemini: { authenticated: false, email: null, loading: true, error: null },
+ kiro: { authenticated: false, email: null, loading: true, error: null },
});
export const readErrorMessageFromResponse = async (response: Response, fallback: string) => {
diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts
index e39a97963..3e555b812 100644
--- a/src/components/provider-auth/types.ts
+++ b/src/components/provider-auth/types.ts
@@ -1 +1 @@
-export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
+export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro';
diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts
index 096059ced..b9c502e14 100644
--- a/src/components/settings/types/types.ts
+++ b/src/components/settings/types/types.ts
@@ -1,7 +1,7 @@
import type { Dispatch, SetStateAction } from 'react';
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins';
-export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
+export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro';
export type AgentCategory = 'account' | 'permissions' | 'mcp';
export type ProjectSortOrder = 'name' | 'date';
export type SaveStatus = 'success' | 'error' | null;
diff --git a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
index 482418263..01b8dc360 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx
@@ -3,13 +3,14 @@ import SessionProviderLogo from '../../../../../llm-logo-provider/SessionProvide
import type { AgentProvider } from '../../../../types/types';
import type { AgentSelectorSectionProps } from '../types';
-const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini'];
+const AGENT_PROVIDERS: AgentProvider[] = ['claude', 'cursor', 'codex', 'gemini', 'kiro'];
const AGENT_NAMES: Record = {
claude: 'Claude',
cursor: 'Cursor',
codex: 'Codex',
gemini: 'Gemini',
+ kiro: 'Kiro',
};
export default function AgentSelectorSection({
diff --git a/src/components/sidebar/types/types.ts b/src/components/sidebar/types/types.ts
index bab1b665b..4287722ee 100644
--- a/src/components/sidebar/types/types.ts
+++ b/src/components/sidebar/types/types.ts
@@ -44,6 +44,7 @@ export type SessionViewModel = {
isCursorSession: boolean;
isCodexSession: boolean;
isGeminiSession: boolean;
+ isKiroSession: boolean;
isActive: boolean;
sessionName: string;
sessionTime: string;
diff --git a/src/components/sidebar/utils/utils.ts b/src/components/sidebar/utils/utils.ts
index a3ec377c0..b125867a3 100644
--- a/src/components/sidebar/utils/utils.ts
+++ b/src/components/sidebar/utils/utils.ts
@@ -64,6 +64,10 @@ export const getSessionName = (session: SessionWithProvider, t: TFunction): stri
return session.summary || session.name || t('projects.newSession');
}
+ if (session.__provider === 'kiro') {
+ return session.summary || session.name || t('projects.newSession');
+ }
+
return session.summary || t('projects.newSession');
};
@@ -91,6 +95,7 @@ export const createSessionViewModel = (
isCursorSession: session.__provider === 'cursor',
isCodexSession: session.__provider === 'codex',
isGeminiSession: session.__provider === 'gemini',
+ isKiroSession: session.__provider === 'kiro',
isActive: diffInMinutes < 10,
sessionName: getSessionName(session, t),
sessionTime: getSessionTime(session),
@@ -122,7 +127,12 @@ export const getAllSessions = (
__provider: 'gemini' as const,
}));
- return [...claudeSessions, ...cursorSessions, ...codexSessions, ...geminiSessions].sort(
+ const kiroSessions = (project.kiroSessions || []).map((session) => ({
+ ...session,
+ __provider: 'kiro' as const,
+ }));
+
+ return [...claudeSessions, ...cursorSessions, ...codexSessions, ...geminiSessions, ...kiroSessions].sort(
(a, b) => getSessionDate(b).getTime() - getSessionDate(a).getTime(),
);
};
diff --git a/src/hooks/useProjectsState.ts b/src/hooks/useProjectsState.ts
index 28cf682e7..38a5bd864 100644
--- a/src/hooks/useProjectsState.ts
+++ b/src/hooks/useProjectsState.ts
@@ -58,7 +58,8 @@ const projectsHaveChanges = (
return (
serialize(nextProject.cursorSessions) !== serialize(prevProject.cursorSessions) ||
serialize(nextProject.codexSessions) !== serialize(prevProject.codexSessions) ||
- serialize(nextProject.geminiSessions) !== serialize(prevProject.geminiSessions)
+ serialize(nextProject.geminiSessions) !== serialize(prevProject.geminiSessions) ||
+ serialize(nextProject.kiroSessions) !== serialize(prevProject.kiroSessions)
);
});
};
@@ -69,6 +70,7 @@ const getProjectSessions = (project: Project): ProjectSession[] => {
...(project.codexSessions ?? []),
...(project.cursorSessions ?? []),
...(project.geminiSessions ?? []),
+ ...(project.kiroSessions ?? []),
];
};
@@ -368,6 +370,21 @@ export function useProjectsState({
}
return;
}
+
+ const kiroSession = project.kiroSessions?.find((session) => session.id === sessionId);
+ if (kiroSession) {
+ const shouldUpdateProject = selectedProject?.name !== project.name;
+ const shouldUpdateSession =
+ selectedSession?.id !== sessionId || selectedSession.__provider !== 'kiro';
+
+ if (shouldUpdateProject) {
+ setSelectedProject(project);
+ }
+ if (shouldUpdateSession) {
+ setSelectedSession({ ...kiroSession, __provider: 'kiro' });
+ }
+ return;
+ }
}
}, [sessionId, projects, selectedProject?.name, selectedSession?.id, selectedSession?.__provider]);
diff --git a/src/types/app.ts b/src/types/app.ts
index 9abaac6be..b43777a52 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,4 +1,4 @@
-export type SessionProvider = 'claude' | 'cursor' | 'codex' | 'gemini';
+export type SessionProvider = 'claude' | 'cursor' | 'codex' | 'gemini' | 'kiro';
export type AppTab = 'chat' | 'files' | 'shell' | 'git' | 'tasks' | 'preview' | `plugin:${string}`;
@@ -39,6 +39,7 @@ export interface Project {
cursorSessions?: ProjectSession[];
codexSessions?: ProjectSession[];
geminiSessions?: ProjectSession[];
+ kiroSessions?: ProjectSession[];
sessionMeta?: ProjectSessionMeta;
taskmaster?: ProjectTaskmasterInfo;
[key: string]: unknown;
From 2302fa6439bdb5ad7bddb3a4cb9a8760e608a919 Mon Sep 17 00:00:00 2001
From: Davide Gallitelli
Date: Mon, 23 Mar 2026 04:49:59 +0000
Subject: [PATCH 2/5] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?=
=?UTF-8?q?=E2=80=94=20correct=20CLI=20flags,=20pagination,=20JSONL=20pars?=
=?UTF-8?q?ing?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes all issues raised in the PR review:
- Use correct Kiro CLI flags: kiro chat --no-interactive [--resume ]
[--agent ] per official docs. Removes non-existent
--prompt, --model, --output-format, --yolo flags.
- Optional watcher path: don't mkdir ~/.kiro/sessions on startup for users
who don't have Kiro installed (optional: true in PROVIDER_WATCH_PATHS)
- Fix JSONL parsing: parse line-by-line in getKiroSessions() and
getKiroCliSessionMessages() instead of whole-file JSON.parse
- Filter unscoped sessions: skip Kiro sessions with no projectPath to
prevent them appearing under every project
- Honor pagination contract in fetchHistory (limit/offset/hasMore/total)
- Enable resume for disk-discovered sessions (fall back to sessionId directly)
- Move processKey declaration before notifyTerminalState closure
- Flush lineBuffer on process close to prevent truncated responses
- Add Kiro label to provider text chains in ChatInterface.tsx
- Type KiroLogo props for TypeScript strict mode
- Add ownership-check TODO in routes/kiro.js (consistent with gemini pattern)
Co-Authored-By: Claude Sonnet 4.6
---
server/index.js | 22 +++++++--
server/kiro-cli.js | 47 ++++++++++---------
server/projects.js | 28 ++++++++++-
server/providers/kiro/adapter.js | 14 ++++--
server/routes/kiro.js | 2 +
src/components/chat/view/ChatInterface.tsx | 8 +++-
src/components/llm-logo-provider/KiroLogo.tsx | 6 ++-
7 files changed, 90 insertions(+), 37 deletions(-)
diff --git a/server/index.js b/server/index.js
index 307925788..640041f2b 100755
--- a/server/index.js
+++ b/server/index.js
@@ -86,7 +86,8 @@ const PROVIDER_WATCH_PATHS = [
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
{ provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') },
// TODO: verify actual Kiro session storage path (~/.kiro/sessions/ is best guess)
- { provider: 'kiro', rootPath: path.join(os.homedir(), '.kiro', 'sessions') }
+ // optional: true — skip mkdir on startup so we don't create ~/.kiro/sessions for users who don't have Kiro installed
+ { provider: 'kiro', rootPath: path.join(os.homedir(), '.kiro', 'sessions'), optional: true }
];
const WATCHER_IGNORED_PATTERNS = [
'**/node_modules/**',
@@ -180,11 +181,22 @@ async function setupProjectsWatcher() {
}, WATCHER_DEBOUNCE_MS);
};
- for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
+ for (const { provider, rootPath, optional } of PROVIDER_WATCH_PATHS) {
try {
- // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
- // Ensure provider folders exist before creating the watcher so watching stays active.
- await fsPromises.mkdir(rootPath, { recursive: true });
+ if (optional) {
+ // For optional providers (e.g. Kiro), skip mkdir so we don't create directories
+ // for users who don't have the provider installed. Only watch if the path exists.
+ try {
+ await fsPromises.access(rootPath);
+ } catch {
+ // Path doesn't exist and is optional — skip watching entirely
+ continue;
+ }
+ } else {
+ // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
+ // Ensure provider folders exist before creating the watcher so watching stays active.
+ await fsPromises.mkdir(rootPath, { recursive: true });
+ }
// Initialize chokidar watcher with optimized settings
const watcher = chokidar.watch(rootPath, {
diff --git a/server/kiro-cli.js b/server/kiro-cli.js
index 1f3606a19..88d5d5f98 100644
--- a/server/kiro-cli.js
+++ b/server/kiro-cli.js
@@ -25,21 +25,19 @@ async function spawnKiro(command, options = {}, ws) {
};
// Build Kiro CLI command arguments
- // TODO: verify actual Kiro CLI argument format — modeled after Gemini CLI structure
- const args = [];
-
- // Add prompt if we have a command
- if (command && command.trim()) {
- // TODO: verify correct Kiro CLI flag for passing a prompt (--prompt, --message, or positional arg)
- args.push('--prompt', command);
- }
+ // Real Kiro CLI interface: kiro chat --no-interactive [--resume ] [--agent ]
+ const args = ['chat', '--no-interactive'];
// If we have a sessionId, attempt to resume
if (sessionId) {
const session = sessionManager.getSession(sessionId);
if (session && session.cliSessionId) {
- // TODO: verify correct Kiro CLI flag for resuming a session (--resume, --session, etc.)
args.push('--resume', session.cliSessionId);
+ } else {
+ // TODO: verify native Kiro session ID format to confirm direct resume is valid
+ // Sessions discovered from disk by getKiroSessions() are not in sessionManager,
+ // so use sessionId directly as the resume value for disk-discovered sessions.
+ args.push('--resume', sessionId);
}
}
@@ -48,19 +46,14 @@ async function spawnKiro(command, options = {}, ws) {
const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim();
const workingDir = cleanPath;
- // TODO: verify if Kiro CLI has a model selection flag
+ // Use --agent flag if a model/agent name is specified
if (options.model) {
- args.push('--model', options.model);
+ args.push('--agent', options.model);
}
- // TODO: verify Kiro CLI output format flag (--output-format, --json, etc.)
- // Assuming JSON line output similar to Gemini CLI
- args.push('--output-format', 'stream-json');
-
- // Handle permission/approval modes
- // TODO: verify Kiro CLI permission mode flags
- if (settings.skipPermissions || options.skipPermissions || permissionMode === 'yolo') {
- args.push('--yolo');
+ // Pass the user message as a positional argument
+ if (command && command.trim()) {
+ args.push(command);
}
// Try to find kiro in PATH first, then fall back to environment variable
@@ -88,6 +81,10 @@ async function spawnKiro(command, options = {}, ws) {
let terminalNotificationSent = false;
let terminalFailureReason = null;
+ // Store process reference for potential abort
+ // processKey is declared before notifyTerminalState so the closure captures a stable variable
+ const processKey = capturedSessionId || sessionId || Date.now().toString();
+
const notifyTerminalState = ({ code = null, error = null } = {}) => {
if (terminalNotificationSent) {
return;
@@ -115,9 +112,6 @@ async function spawnKiro(command, options = {}, ws) {
error: error || terminalFailureReason || `Kiro CLI exited with code ${code}`
});
};
-
- // Store process reference for potential abort
- const processKey = capturedSessionId || sessionId || Date.now().toString();
activeKiroProcesses.set(processKey, kiroProcess);
// Store sessionId on the process object for debugging
@@ -239,6 +233,15 @@ async function spawnKiro(command, options = {}, ws) {
kiroProcess.on('close', async (code) => {
clearTimeout(timeout);
+ // Flush any remaining lineBuffer content that wasn't terminated by a newline
+ if (lineBuffer.trim()) {
+ const content = lineBuffer.trim();
+ lineBuffer = '';
+ // treat as raw text - send as stream_delta
+ const socketSessionId = capturedSessionId || sessionId;
+ ws.send(createNormalizedMessage({ kind: 'stream_delta', content, sessionId: socketSessionId, provider: 'kiro' }));
+ }
+
// Clean up process reference
const finalSessionId = capturedSessionId || sessionId || processKey;
activeKiroProcesses.delete(finalSessionId);
diff --git a/server/projects.js b/server/projects.js
index 56b05d1c3..97b0724c5 100755
--- a/server/projects.js
+++ b/server/projects.js
@@ -2597,9 +2597,19 @@ async function getKiroSessions(projectPath) {
const data = await fs.readFile(filePath, 'utf8');
// TODO: verify actual Kiro session file format — using best-guess field names
+ // Parse the file: .jsonl files are line-delimited JSON (one object per line),
+ // .json files are a single JSON blob.
let session;
try {
- session = JSON.parse(data);
+ if (sessionFile.endsWith('.jsonl')) {
+ // JSONL: each line is a separate JSON object; treat first non-empty parsed line as session metadata
+ const lines = data.split('\n').filter(l => l.trim());
+ const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
+ // Reconstruct a session-like object from the lines array
+ session = parsed.length === 1 ? parsed[0] : { messages: parsed };
+ } else {
+ session = JSON.parse(data);
+ }
} catch {
continue;
}
@@ -2611,6 +2621,12 @@ async function getKiroSessions(projectPath) {
}
}
+ // Skip sessions with no projectPath — they can't be associated with a specific project.
+ // TODO: include unscoped sessions in a global bucket once project association is confirmed
+ if (!session.projectPath) {
+ continue;
+ }
+
const sessionId = session.sessionId || session.id || sessionFile.replace(/\.(json|jsonl)$/, '');
const messages = session.messages || [];
const firstUserMsg = messages.find(m => m.role === 'user' || m.type === 'user');
@@ -2663,7 +2679,15 @@ async function getKiroCliSessionMessages(sessionId) {
try {
const filePath = path.join(kiroSessionsDir, sessionFile);
const data = await fs.readFile(filePath, 'utf8');
- const session = JSON.parse(data);
+ // Parse the file: .jsonl files are line-delimited JSON, .json files are a single blob.
+ let session;
+ if (sessionFile.endsWith('.jsonl')) {
+ const lines = data.split('\n').filter(l => l.trim());
+ const parsed = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
+ session = parsed.length === 1 ? parsed[0] : { messages: parsed };
+ } else {
+ session = JSON.parse(data);
+ }
const fileSessionId = session.sessionId || session.id || sessionFile.replace(/\.(json|jsonl)$/, '');
if (fileSessionId !== sessionId) continue;
diff --git a/server/providers/kiro/adapter.js b/server/providers/kiro/adapter.js
index aa54bce9c..00a5617e0 100644
--- a/server/providers/kiro/adapter.js
+++ b/server/providers/kiro/adapter.js
@@ -101,6 +101,7 @@ export const kiroAdapter = {
* TODO: verify actual Kiro session storage path once CLI is available.
*/
async fetchHistory(sessionId, opts = {}) {
+ const { limit = null, offset = 0 } = opts;
let rawMessages;
try {
rawMessages = sessionManager.getSessionMessages(sessionId);
@@ -193,12 +194,15 @@ export const kiroAdapter = {
}
}
+ const total = normalized.length;
+ const sliced = limit !== null ? normalized.slice(offset, offset + limit) : normalized.slice(offset);
+
return {
- messages: normalized,
- total: normalized.length,
- hasMore: false,
- offset: 0,
- limit: null,
+ messages: sliced,
+ total,
+ hasMore: limit !== null ? offset + limit < total : false,
+ offset,
+ limit,
};
},
};
diff --git a/server/routes/kiro.js b/server/routes/kiro.js
index 97b7e9d88..d3a3eebf1 100644
--- a/server/routes/kiro.js
+++ b/server/routes/kiro.js
@@ -5,6 +5,8 @@ import { sessionNamesDb } from '../database/db.js';
const router = express.Router();
router.delete('/sessions/:sessionId', async (req, res) => {
+ // TODO: add per-user ownership check (see #574)
+ // Note: gemini.js uses the same pattern without ownership checks — keeping consistent.
try {
const { sessionId } = req.params;
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
index 3e242db60..96463a659 100644
--- a/src/components/chat/view/ChatInterface.tsx
+++ b/src/components/chat/view/ChatInterface.tsx
@@ -279,7 +279,9 @@ function ChatInterface({
? t('messageTypes.codex')
: provider === 'gemini'
? t('messageTypes.gemini')
- : t('messageTypes.claude');
+ : provider === 'kiro'
+ ? t('messageTypes.kiro')
+ : t('messageTypes.claude');
return (
@@ -409,7 +411,9 @@ function ChatInterface({
? t('messageTypes.codex')
: provider === 'gemini'
? t('messageTypes.gemini')
- : t('messageTypes.claude'),
+ : provider === 'kiro'
+ ? t('messageTypes.kiro')
+ : t('messageTypes.claude'),
})}
isTextareaExpanded={isTextareaExpanded}
sendByCtrlEnter={sendByCtrlEnter}
diff --git a/src/components/llm-logo-provider/KiroLogo.tsx b/src/components/llm-logo-provider/KiroLogo.tsx
index f0fbc3f22..1c8521875 100644
--- a/src/components/llm-logo-provider/KiroLogo.tsx
+++ b/src/components/llm-logo-provider/KiroLogo.tsx
@@ -1,4 +1,8 @@
-const KiroLogo = ({className = 'w-5 h-5'}) => {
+interface KiroLogoProps {
+ className?: string;
+}
+
+const KiroLogo = ({ className = 'w-5 h-5' }: KiroLogoProps) => {
// TODO: replace with official Kiro icon once available at /icons/kiro-icon.svg
// Kiro is AWS's agentic IDE built on Claude (https://kiro.dev)
// For now, render a simple "K" text badge as a fallback
From 4b1070d948cb06054d36565df6e8aa8375b61106 Mon Sep 17 00:00:00 2001
From: Davide Gallitelli
Date: Mon, 23 Mar 2026 05:05:47 +0000
Subject: [PATCH 3/5] fix: add missing Kiro i18n keys across all 6 locales
Adds all missing provider keys for Kiro in every locale file:
- messageTypes.kiro in chat.json (en, de, ko, ru, zh-CN, ja)
- providerSelection.providerInfo.kiro in chat.json (all 6)
- providerSelection.readyPrompt.kiro in chat.json (all 6, locale-appropriate)
- agents.account.kiro.description in settings.json (all 6)
- Defensive { defaultValue: 'Kiro' } on both t('messageTypes.kiro') calls
in ChatInterface.tsx
Also fills pre-existing ja locale gaps (missing gemini in messageTypes,
readyPrompt, and providerInfo.google) for consistency.
Co-Authored-By: Claude Sonnet 4.6
---
src/components/chat/view/ChatInterface.tsx | 4 ++--
src/i18n/locales/de/chat.json | 7 +++++--
src/i18n/locales/de/settings.json | 3 +++
src/i18n/locales/en/chat.json | 7 +++++--
src/i18n/locales/en/settings.json | 3 +++
src/i18n/locales/ja/chat.json | 10 ++++++++--
src/i18n/locales/ja/settings.json | 3 +++
src/i18n/locales/ko/chat.json | 7 +++++--
src/i18n/locales/ko/settings.json | 3 +++
src/i18n/locales/ru/chat.json | 7 +++++--
src/i18n/locales/ru/settings.json | 3 +++
src/i18n/locales/zh-CN/chat.json | 7 +++++--
src/i18n/locales/zh-CN/settings.json | 3 +++
13 files changed, 53 insertions(+), 14 deletions(-)
diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx
index 96463a659..2f88fbf50 100644
--- a/src/components/chat/view/ChatInterface.tsx
+++ b/src/components/chat/view/ChatInterface.tsx
@@ -280,7 +280,7 @@ function ChatInterface({
: provider === 'gemini'
? t('messageTypes.gemini')
: provider === 'kiro'
- ? t('messageTypes.kiro')
+ ? t('messageTypes.kiro', { defaultValue: 'Kiro' })
: t('messageTypes.claude');
return (
@@ -412,7 +412,7 @@ function ChatInterface({
: provider === 'gemini'
? t('messageTypes.gemini')
: provider === 'kiro'
- ? t('messageTypes.kiro')
+ ? t('messageTypes.kiro', { defaultValue: 'Kiro' })
: t('messageTypes.claude'),
})}
isTextareaExpanded={isTextareaExpanded}
diff --git a/src/i18n/locales/de/chat.json b/src/i18n/locales/de/chat.json
index 4d978bcf2..ecafd06b6 100644
--- a/src/i18n/locales/de/chat.json
+++ b/src/i18n/locales/de/chat.json
@@ -18,7 +18,8 @@
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
- "gemini": "Gemini"
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "Werkzeugeinstellungen",
@@ -180,13 +181,15 @@
"anthropic": "von Anthropic",
"openai": "von OpenAI",
"cursorEditor": "KI-Code-Editor",
- "google": "von Google"
+ "google": "von Google",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "Bereit, Claude mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"cursor": "Bereit, Cursor mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"codex": "Bereit, Codex mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"gemini": "Bereit, Gemini mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
+ "kiro": "Bereit, Kiro mit {{model}} zu verwenden. Gib unten deine Nachricht ein.",
"default": "Wähl oben einen Anbieter, um zu beginnen"
}
},
diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json
index 25c289dd3..32070ce64 100644
--- a/src/i18n/locales/de/settings.json
+++ b/src/i18n/locales/de/settings.json
@@ -312,6 +312,9 @@
},
"gemini": {
"description": "Google Gemini KI-Assistent"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "Verbindungsstatus",
diff --git a/src/i18n/locales/en/chat.json b/src/i18n/locales/en/chat.json
index dadaea892..1b1d85f98 100644
--- a/src/i18n/locales/en/chat.json
+++ b/src/i18n/locales/en/chat.json
@@ -18,7 +18,8 @@
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
- "gemini": "Gemini"
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "Tool Settings",
@@ -180,13 +181,15 @@
"anthropic": "by Anthropic",
"openai": "by OpenAI",
"cursorEditor": "AI Code Editor",
- "google": "by Google"
+ "google": "by Google",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "Ready to use Claude with {{model}}. Start typing your message below.",
"cursor": "Ready to use Cursor with {{model}}. Start typing your message below.",
"codex": "Ready to use Codex with {{model}}. Start typing your message below.",
"gemini": "Ready to use Gemini with {{model}}. Start typing your message below.",
+ "kiro": "Ready to use Kiro with {{model}}. Start typing your message below.",
"default": "Select a provider above to begin"
}
},
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index fcd1c7287..5e79db386 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -333,6 +333,9 @@
},
"gemini": {
"description": "Google Gemini AI assistant"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "Connection Status",
diff --git a/src/i18n/locales/ja/chat.json b/src/i18n/locales/ja/chat.json
index 10e03192d..cd8d2fc27 100644
--- a/src/i18n/locales/ja/chat.json
+++ b/src/i18n/locales/ja/chat.json
@@ -17,7 +17,9 @@
"tool": "ツール",
"claude": "Claude",
"cursor": "Cursor",
- "codex": "Codex"
+ "codex": "Codex",
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "ツール設定",
@@ -158,12 +160,16 @@
"providerInfo": {
"anthropic": "by Anthropic",
"openai": "by OpenAI",
- "cursorEditor": "AIコードエディタ"
+ "cursorEditor": "AIコードエディタ",
+ "google": "by Google",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "{{model}}でClaudeを使用する準備ができました。下にメッセージを入力してください。",
"cursor": "{{model}}でCursorを使用する準備ができました。下にメッセージを入力してください。",
"codex": "{{model}}でCodexを使用する準備ができました。下にメッセージを入力してください。",
+ "gemini": "{{model}}でGeminiを使用する準備ができました。下にメッセージを入力してください。",
+ "kiro": "{{model}}でKiroを使用する準備ができました。下にメッセージを入力してください。",
"default": "上からプロバイダーを選択して開始してください"
}
},
diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json
index 60b454caf..472ce2f48 100644
--- a/src/i18n/locales/ja/settings.json
+++ b/src/i18n/locales/ja/settings.json
@@ -333,6 +333,9 @@
},
"gemini": {
"description": "Google Gemini AIアシスタント"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "接続状態",
diff --git a/src/i18n/locales/ko/chat.json b/src/i18n/locales/ko/chat.json
index aaf5b45cd..53d2aecd9 100644
--- a/src/i18n/locales/ko/chat.json
+++ b/src/i18n/locales/ko/chat.json
@@ -18,7 +18,8 @@
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
- "gemini": "Gemini"
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "도구 설정",
@@ -162,13 +163,15 @@
"anthropic": "Anthropic 제공",
"openai": "OpenAI 제공",
"cursorEditor": "AI 코드 에디터",
- "google": "Google 제공"
+ "google": "Google 제공",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "{{model}} 모델로 Claude를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"cursor": "{{model}} 모델로 Cursor를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"codex": "{{model}} 모델로 Codex를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"gemini": "{{model}} 모델로 Gemini를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
+ "kiro": "{{model}} 모델로 Kiro를 사용할 준비가 되었습니다. 아래에 메시지를 입력하세요.",
"default": "시작하려면 위에서 제공자를 선택하세요"
}
},
diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json
index b8a1f450d..4fb451def 100644
--- a/src/i18n/locales/ko/settings.json
+++ b/src/i18n/locales/ko/settings.json
@@ -333,6 +333,9 @@
},
"gemini": {
"description": "Google Gemini AI 어시스턴트"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "연결 상태",
diff --git a/src/i18n/locales/ru/chat.json b/src/i18n/locales/ru/chat.json
index a1c5e2773..23757dd30 100644
--- a/src/i18n/locales/ru/chat.json
+++ b/src/i18n/locales/ru/chat.json
@@ -18,7 +18,8 @@
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
- "gemini": "Gemini"
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "Настройки инструмента",
@@ -180,13 +181,15 @@
"anthropic": "от Anthropic",
"openai": "от OpenAI",
"cursorEditor": "AI редактор кода",
- "google": "от Google"
+ "google": "от Google",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "Готов использовать Claude с {{model}}. Начните вводить сообщение ниже.",
"cursor": "Готов использовать Cursor с {{model}}. Начните вводить сообщение ниже.",
"codex": "Готов использовать Codex с {{model}}. Начните вводить сообщение ниже.",
"gemini": "Готов использовать Gemini с {{model}}. Начните вводить сообщение ниже.",
+ "kiro": "Готов использовать Kiro с {{model}}. Начните вводить сообщение ниже.",
"default": "Выберите провайдера выше для начала"
}
},
diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json
index f8991ab0e..98f16ed5c 100644
--- a/src/i18n/locales/ru/settings.json
+++ b/src/i18n/locales/ru/settings.json
@@ -312,6 +312,9 @@
},
"gemini": {
"description": "AI-ассистент Google Gemini"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "Статус подключения",
diff --git a/src/i18n/locales/zh-CN/chat.json b/src/i18n/locales/zh-CN/chat.json
index 1d224cc76..0accb4ae7 100644
--- a/src/i18n/locales/zh-CN/chat.json
+++ b/src/i18n/locales/zh-CN/chat.json
@@ -18,7 +18,8 @@
"claude": "Claude",
"cursor": "Cursor",
"codex": "Codex",
- "gemini": "Gemini"
+ "gemini": "Gemini",
+ "kiro": "Kiro"
},
"tools": {
"settings": "工具设置",
@@ -162,13 +163,15 @@
"anthropic": "由 Anthropic 提供",
"openai": "由 OpenAI 提供",
"cursorEditor": "AI 代码编辑器",
- "google": "由 Google 提供"
+ "google": "由 Google 提供",
+ "kiro": "AWS Agentic IDE"
},
"readyPrompt": {
"claude": "准备好使用带有 {{model}} 的 Claude。请在下方开始输入您的消息。",
"cursor": "准备好使用带有 {{model}} 的 Cursor。请在下方开始输入您的消息。",
"codex": "准备好使用带有 {{model}} 的 Codex。请在下方开始输入您的消息。",
"gemini": "准备好使用带有 {{model}} 的 Gemini。请在下方开始输入您的消息。",
+ "kiro": "准备好使用带有 {{model}} 的 Kiro。请在下方开始输入您的消息。",
"default": "请在上方选择一个提供者以开始"
}
},
diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json
index d9f2b2cda..8c528f8b3 100644
--- a/src/i18n/locales/zh-CN/settings.json
+++ b/src/i18n/locales/zh-CN/settings.json
@@ -333,6 +333,9 @@
},
"gemini": {
"description": "Google Gemini AI 助手"
+ },
+ "kiro": {
+ "description": "Amazon Kiro agentic IDE"
}
},
"connectionStatus": "连接状态",
From 61f173688c88a8e13bac0a16658417381a3f263b Mon Sep 17 00:00:00 2001
From: Davide Gallitelli
Date: Mon, 23 Mar 2026 06:51:39 +0000
Subject: [PATCH 4/5] fix: resolve TypeScript errors in settings agent config
records
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adding 'kiro' to AgentProvider required updating all Record
objects that TypeScript now enforces exhaustively:
- settings/constants/constants.ts — add kiro to AUTH_STATUS_ENDPOINTS
- AgentListItem.tsx — add kiro to agentConfig (name/color)
- AgentsSettingsTab.tsx — add kiro stub to agentContextById (no auth flow yet)
- AccountContent.tsx — add kiro to agentConfig (name/colors/description)
All 4 TypeScript errors are resolved. Build and typecheck now pass cleanly.
Co-Authored-By: Claude Sonnet 4.6
---
src/components/settings/constants/constants.ts | 1 +
.../settings/view/tabs/agents-settings/AgentListItem.tsx | 6 +++++-
.../view/tabs/agents-settings/AgentsSettingsTab.tsx | 4 ++++
.../agents-settings/sections/content/AccountContent.tsx | 9 +++++++++
4 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts
index 36f45392d..8b420b830 100644
--- a/src/components/settings/constants/constants.ts
+++ b/src/components/settings/constants/constants.ts
@@ -93,4 +93,5 @@ export const AUTH_STATUS_ENDPOINTS: Record = {
cursor: '/api/cli/cursor/status',
codex: '/api/cli/codex/status',
gemini: '/api/cli/gemini/status',
+ kiro: '/api/cli/kiro/status',
};
diff --git a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
index 47135939f..b86d23e13 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentListItem.tsx
@@ -31,7 +31,11 @@ const agentConfig: Record = {
gemini: {
name: 'Gemini',
color: 'indigo',
- }
+ },
+ kiro: {
+ name: 'Kiro',
+ color: 'gray',
+ },
};
const colorClasses = {
diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
index dbf098cb1..c1c5597e5 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
@@ -56,6 +56,10 @@ export default function AgentsSettingsTab({
authStatus: geminiAuthStatus,
onLogin: onGeminiLogin,
},
+ kiro: {
+ authStatus: { authenticated: false, email: null, loading: false, error: null },
+ onLogin: () => {},
+ },
}), [
claudeAuthStatus,
codexAuthStatus,
diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
index dcf4baf14..889a06680 100644
--- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
+++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx
@@ -54,6 +54,15 @@ const agentConfig: Record = {
subtextClass: 'text-indigo-700 dark:text-indigo-300',
buttonClass: 'bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800',
},
+ kiro: {
+ name: 'Kiro',
+ description: 'Amazon Kiro agentic IDE',
+ bgClass: 'bg-orange-50 dark:bg-orange-900/20',
+ borderClass: 'border-orange-200 dark:border-orange-800',
+ textClass: 'text-orange-900 dark:text-orange-100',
+ subtextClass: 'text-orange-700 dark:text-orange-300',
+ buttonClass: 'bg-orange-600 hover:bg-orange-700 active:bg-orange-800',
+ },
};
export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
From 40c2cedd444c02227fcdf70d103f21cc1e4fe238 Mon Sep 17 00:00:00 2001
From: Davide Gallitelli
Date: Mon, 23 Mar 2026 08:01:01 +0000
Subject: [PATCH 5/5] fix: wire Kiro auth through all 4 settings layers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replaces the no-op Kiro stub with real props-driven auth state, following
the exact gemini pattern at every layer:
- useSettingsController.ts: add kiroAuthStatus state, fix
setAuthStatusByProvider (kiro was falling through to setCodexAuthStatus —
a real bug), add mount-time checkAuthStatus('kiro'), expose in return
- AgentsSettingsTabProps types.ts: add kiroAuthStatus + onKiroLogin props
- AgentsSettingsTab.tsx: wire props into agentContextById.kiro + useMemo deps
- Settings.tsx: pass kiroAuthStatus + onKiroLogin={openLoginForProvider('kiro')},
add kiro branch to isAuthenticated ternary (also fixes missing gemini branch)
typecheck passes cleanly.
Co-Authored-By: Claude Sonnet 4.6
---
src/components/settings/hooks/useSettingsController.ts | 5 +++++
src/components/settings/view/Settings.tsx | 9 ++++++++-
.../view/tabs/agents-settings/AgentsSettingsTab.tsx | 8 ++++++--
.../settings/view/tabs/agents-settings/types.ts | 2 ++
4 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts
index 293cbceb5..dbab55e58 100644
--- a/src/components/settings/hooks/useSettingsController.ts
+++ b/src/components/settings/hooks/useSettingsController.ts
@@ -248,6 +248,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
const [cursorAuthStatus, setCursorAuthStatus] = useState(DEFAULT_AUTH_STATUS);
const [codexAuthStatus, setCodexAuthStatus] = useState(DEFAULT_AUTH_STATUS);
const [geminiAuthStatus, setGeminiAuthStatus] = useState(DEFAULT_AUTH_STATUS);
+ const [kiroAuthStatus, setKiroAuthStatus] = useState(DEFAULT_AUTH_STATUS);
const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => {
if (provider === 'claude') {
@@ -265,6 +266,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
return;
}
+ if (provider === 'kiro') { setKiroAuthStatus(status); return; }
+
setCodexAuthStatus(status);
}, []);
@@ -831,6 +834,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
void checkAuthStatus('cursor');
void checkAuthStatus('codex');
void checkAuthStatus('gemini');
+ void checkAuthStatus('kiro');
}, [checkAuthStatus, initialTab, isOpen, loadSettings]);
useEffect(() => {
@@ -939,6 +943,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }:
cursorAuthStatus,
codexAuthStatus,
geminiAuthStatus,
+ kiroAuthStatus,
geminiPermissionMode,
setGeminiPermissionMode,
openLoginForProvider,
diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx
index 444d0e060..636ab42cc 100644
--- a/src/components/settings/view/Settings.tsx
+++ b/src/components/settings/view/Settings.tsx
@@ -59,6 +59,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
cursorAuthStatus,
codexAuthStatus,
geminiAuthStatus,
+ kiroAuthStatus,
geminiPermissionMode,
setGeminiPermissionMode,
openLoginForProvider,
@@ -110,7 +111,11 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
? cursorAuthStatus.authenticated
: loginProvider === 'codex'
? codexAuthStatus.authenticated
- : false;
+ : loginProvider === 'gemini'
+ ? geminiAuthStatus.authenticated
+ : loginProvider === 'kiro'
+ ? kiroAuthStatus.authenticated
+ : false;
return (
@@ -161,10 +166,12 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
cursorAuthStatus={cursorAuthStatus}
codexAuthStatus={codexAuthStatus}
geminiAuthStatus={geminiAuthStatus}
+ kiroAuthStatus={kiroAuthStatus}
onClaudeLogin={() => openLoginForProvider('claude')}
onCursorLogin={() => openLoginForProvider('cursor')}
onCodexLogin={() => openLoginForProvider('codex')}
onGeminiLogin={() => openLoginForProvider('gemini')}
+ onKiroLogin={() => openLoginForProvider('kiro')}
claudePermissions={claudePermissions}
onClaudePermissionsChange={setClaudePermissions}
cursorPermissions={cursorPermissions}
diff --git a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
index c1c5597e5..1b8225300 100644
--- a/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
+++ b/src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx
@@ -10,10 +10,12 @@ export default function AgentsSettingsTab({
cursorAuthStatus,
codexAuthStatus,
geminiAuthStatus,
+ kiroAuthStatus,
onClaudeLogin,
onCursorLogin,
onCodexLogin,
onGeminiLogin,
+ onKiroLogin,
claudePermissions,
onClaudePermissionsChange,
cursorPermissions,
@@ -57,18 +59,20 @@ export default function AgentsSettingsTab({
onLogin: onGeminiLogin,
},
kiro: {
- authStatus: { authenticated: false, email: null, loading: false, error: null },
- onLogin: () => {},
+ authStatus: kiroAuthStatus,
+ onLogin: onKiroLogin,
},
}), [
claudeAuthStatus,
codexAuthStatus,
cursorAuthStatus,
geminiAuthStatus,
+ kiroAuthStatus,
onClaudeLogin,
onCodexLogin,
onCursorLogin,
onGeminiLogin,
+ onKiroLogin,
]);
return (
diff --git a/src/components/settings/view/tabs/agents-settings/types.ts b/src/components/settings/view/tabs/agents-settings/types.ts
index 4bf0c5c9a..245184f34 100644
--- a/src/components/settings/view/tabs/agents-settings/types.ts
+++ b/src/components/settings/view/tabs/agents-settings/types.ts
@@ -23,10 +23,12 @@ export type AgentsSettingsTabProps = {
cursorAuthStatus: AuthStatus;
codexAuthStatus: AuthStatus;
geminiAuthStatus: AuthStatus;
+ kiroAuthStatus: AuthStatus;
onClaudeLogin: () => void;
onCursorLogin: () => void;
onCodexLogin: () => void;
onGeminiLogin: () => void;
+ onKiroLogin: () => void;
claudePermissions: ClaudePermissionsState;
onClaudePermissionsChange: (value: ClaudePermissionsState) => void;
cursorPermissions: CursorPermissionsState;