diff --git a/server/index.js b/server/index.js index 5318fb359..640041f2b 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,10 @@ 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) + // 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/**', @@ -176,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, { @@ -395,6 +411,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 +1532,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 +1557,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 +1593,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 +1628,8 @@ function handleChatConnection(ws, request) { claude: getActiveClaudeSDKSessions(), cursor: getActiveCursorSessions(), codex: getActiveCodexSessions(), - gemini: getActiveGeminiSessions() + gemini: getActiveGeminiSessions(), + kiro: getActiveKiroSessions() }; writer.send({ type: 'active-sessions', @@ -2249,6 +2279,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..88d5d5f98 --- /dev/null +++ b/server/kiro-cli.js @@ -0,0 +1,330 @@ +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 + // 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) { + 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); + } + } + + // 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; + + // Use --agent flag if a model/agent name is specified + if (options.model) { + args.push('--agent', options.model); + } + + // 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 + 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; + + // 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; + } + + 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}` + }); + }; + 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); + + // 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); + + // 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..97b0724c5 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,164 @@ 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 + // Parse the file: .jsonl files are line-delimited JSON (one object per line), + // .json files are a single JSON blob. + let session; + try { + 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; + } + + // If session has a projectPath field, filter by it + if (session.projectPath && normalizedProjectPath) { + if (normalizeComparablePath(session.projectPath) !== normalizedProjectPath) { + continue; + } + } + + // 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'); + 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'); + // 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; + + // 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 +2737,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..00a5617e0 --- /dev/null +++ b/server/providers/kiro/adapter.js @@ -0,0 +1,208 @@ +/** + * 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 = {}) { + const { limit = null, offset = 0 } = 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 }; + } + } + + const total = normalized.length; + const sliced = limit !== null ? normalized.slice(offset, offset + limit) : normalized.slice(offset); + + return { + messages: sliced, + total, + hasMore: limit !== null ? offset + limit < total : false, + offset, + limit, + }; + }, +}; 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..d3a3eebf1 --- /dev/null +++ b/server/routes/kiro.js @@ -0,0 +1,26 @@ +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) => { + // 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; + + 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..2f88fbf50 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, @@ -276,7 +279,9 @@ function ChatInterface({ ? t('messageTypes.codex') : provider === 'gemini' ? t('messageTypes.gemini') - : t('messageTypes.claude'); + : provider === 'kiro' + ? t('messageTypes.kiro', { defaultValue: 'Kiro' }) + : t('messageTypes.claude'); return (
@@ -314,6 +319,8 @@ function ChatInterface({ setCodexModel={setCodexModel} geminiModel={geminiModel} setGeminiModel={setGeminiModel} + kiroModel={kiroModel} + setKiroModel={setKiroModel} tasksEnabled={tasksEnabled} isTaskMasterInstalled={isTaskMasterInstalled} onShowAllTasks={onShowAllTasks} @@ -404,7 +411,9 @@ function ChatInterface({ ? t('messageTypes.codex') : provider === 'gemini' ? t('messageTypes.gemini') - : t('messageTypes.claude'), + : provider === 'kiro' + ? t('messageTypes.kiro', { defaultValue: 'Kiro' }) + : t('messageTypes.claude'), })} isTextareaExpanded={isTextareaExpanded} sendByCtrlEnter={sendByCtrlEnter} 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..1c8521875 --- /dev/null +++ b/src/components/llm-logo-provider/KiroLogo.tsx @@ -0,0 +1,20 @@ +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 + 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/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/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/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/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/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..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, @@ -56,15 +58,21 @@ export default function AgentsSettingsTab({ authStatus: geminiAuthStatus, onLogin: onGeminiLogin, }, + kiro: { + 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/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/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) { 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; 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/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": "连接状态", 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;