CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
ai-chat.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 3ef4c9d | 1 | /** |
| 2 | * AI chat assistant — conversational interface grounded in repo context. | |
| 3 | * | |
| 4 | * The assistant can: | |
| 5 | * - Answer questions about the codebase ("where is auth handled?") | |
| 6 | * - Explain files / functions ("explain src/lib/gate.ts") | |
| 7 | * - Draft code (answered verbally, user applies manually) | |
| 8 | * - Summarise recent activity ("what changed this week?") | |
| 9 | * | |
| 10 | * Grounds its answers in a curated context window built from: | |
| 11 | * - Repo README | |
| 12 | * - Recent commits | |
| 13 | * - Tree listing | |
| 14 | * - Files the user explicitly @-mentions in the message | |
| 15 | */ | |
| 16 | ||
| 17 | import { | |
| 18 | getAnthropic, | |
| 19 | MODEL_SONNET, | |
| 20 | extractText, | |
| 21 | isAiAvailable, | |
| 22 | } from "./ai-client"; | |
| 23 | import { | |
| 24 | getReadme, | |
| 25 | getTree, | |
| 26 | getBlob, | |
| 27 | listCommits, | |
| 28 | getDefaultBranch, | |
| 29 | } from "../git/repository"; | |
| 30 | ||
| 31 | export interface ChatMessage { | |
| 32 | role: "user" | "assistant"; | |
| 33 | content: string; | |
| 34 | } | |
| 35 | ||
| 36 | export interface ChatResponse { | |
| 37 | reply: string; | |
| 38 | citedFiles: string[]; | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Build a concise repo context block. | |
| 43 | * Keeps under ~60k chars to leave room for conversation. | |
| 44 | */ | |
| 45 | async function buildRepoContext( | |
| 46 | owner: string, | |
| 47 | repo: string, | |
| 48 | mentionedFiles: string[] | |
| 49 | ): Promise<{ context: string; files: string[] }> { | |
| 50 | const branch = (await getDefaultBranch(owner, repo)) || "main"; | |
| 51 | const citedFiles: string[] = []; | |
| 52 | const parts: string[] = []; | |
| 53 | ||
| 54 | parts.push(`# Repository: ${owner}/${repo}\nDefault branch: ${branch}\n`); | |
| 55 | ||
| 56 | // README | |
| 57 | const readme = await getReadme(owner, repo, branch); | |
| 58 | if (readme) { | |
| 59 | parts.push(`## README\n${readme.slice(0, 8000)}\n`); | |
| 60 | } | |
| 61 | ||
| 62 | // Top-level tree | |
| 63 | const tree = await getTree(owner, repo, branch); | |
| 64 | if (tree.length > 0) { | |
| 65 | parts.push( | |
| 66 | `## Top-level files\n${tree | |
| 67 | .slice(0, 60) | |
| 68 | .map((e) => `- ${e.type === "tree" ? e.name + "/" : e.name}`) | |
| 69 | .join("\n")}\n` | |
| 70 | ); | |
| 71 | } | |
| 72 | ||
| 73 | // Recent commits | |
| 74 | const commits = await listCommits(owner, repo, branch, 15); | |
| 75 | if (commits.length > 0) { | |
| 76 | parts.push( | |
| 77 | `## Recent commits\n${commits | |
| 78 | .map((c) => `- ${c.sha.slice(0, 7)} ${c.message.split("\n")[0]} — ${c.author}`) | |
| 79 | .join("\n")}\n` | |
| 80 | ); | |
| 81 | } | |
| 82 | ||
| 83 | // Mentioned files | |
| 84 | for (const file of mentionedFiles.slice(0, 8)) { | |
| 85 | try { | |
| 86 | const blob = await getBlob(owner, repo, branch, file); | |
| 87 | if (blob && !blob.isBinary) { | |
| 88 | citedFiles.push(file); | |
| 89 | parts.push(`## File: ${file}\n\`\`\`\n${blob.content.slice(0, 12000)}\n\`\`\`\n`); | |
| 90 | } | |
| 91 | } catch { | |
| 92 | // ignore | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | return { context: parts.join("\n"), files: citedFiles }; | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * Extract @-mentions of files from a user's message. | |
| 101 | * Supports @filename.ext and @path/to/file.ext. | |
| 102 | */ | |
| 103 | function extractFileMentions(text: string): string[] { | |
| 104 | const matches = text.match(/@([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)/g); | |
| 105 | if (!matches) return []; | |
| 106 | return Array.from(new Set(matches.map((m) => m.slice(1)))); | |
| 107 | } | |
| 108 | ||
| 109 | export async function chat( | |
| 110 | owner: string, | |
| 111 | repo: string | null, | |
| 112 | history: ChatMessage[], | |
| 113 | userMessage: string | |
| 114 | ): Promise<ChatResponse> { | |
| 115 | if (!isAiAvailable()) { | |
| 116 | return { | |
| 117 | reply: | |
| 118 | "AI chat is not available — the server needs an ANTHROPIC_API_KEY to be configured.", | |
| 119 | citedFiles: [], | |
| 120 | }; | |
| 121 | } | |
| 122 | const client = getAnthropic(); | |
| 123 | ||
| 124 | const mentioned = extractFileMentions(userMessage); | |
| 125 | const { context: repoContext, files } = repo | |
| 126 | ? await buildRepoContext(owner, repo, mentioned) | |
| 127 | : { context: "", files: [] }; | |
| 128 | ||
| 129 | const system = repo | |
| 130 | ? `You are GlueCron's AI assistant. You help developers understand and work with the repository ${owner}/${repo}. Be concise, accurate, and reference specific files and line numbers when relevant. If the user asks about something not in your context, say so.` | |
| 131 | : `You are GlueCron's AI assistant. You help developers navigate the GlueCron platform — a git host with green-gate enforcement, AI code review, and auto-repair. Keep answers concise.`; | |
| 132 | ||
| 133 | const messages: ChatMessage[] = [ | |
| 134 | ...history, | |
| 135 | { | |
| 136 | role: "user", | |
| 137 | content: repoContext | |
| 138 | ? `${repoContext}\n\n---\n\nUser question: ${userMessage}` | |
| 139 | : userMessage, | |
| 140 | }, | |
| 141 | ]; | |
| 142 | ||
| 143 | const response = await client.messages.create({ | |
| 144 | model: MODEL_SONNET, | |
| 145 | max_tokens: 2048, | |
| 146 | system, | |
| 147 | messages: messages.map((m) => ({ role: m.role, content: m.content })), | |
| 148 | }); | |
| 149 | ||
| 150 | return { | |
| 151 | reply: extractText(response).trim(), | |
| 152 | citedFiles: files, | |
| 153 | }; | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Explain a single file — used for the "Explain this file" button on blob views. | |
| 158 | */ | |
| 159 | export async function explainFile( | |
| 160 | owner: string, | |
| 161 | repo: string, | |
| 162 | filePath: string, | |
| 163 | content: string | |
| 164 | ): Promise<string> { | |
| 165 | if (!isAiAvailable()) { | |
| 166 | return "AI explanations are not available — server needs ANTHROPIC_API_KEY."; | |
| 167 | } | |
| 168 | const client = getAnthropic(); | |
| 169 | const message = await client.messages.create({ | |
| 170 | model: MODEL_SONNET, | |
| 171 | max_tokens: 1024, | |
| 172 | messages: [ | |
| 173 | { | |
| 174 | role: "user", | |
| 175 | content: `Explain this file from ${owner}/${repo} in plain English. | |
| 176 | ||
| 177 | Structure: | |
| 178 | 1. **Purpose** — one sentence | |
| 179 | 2. **Key exports / APIs** — bulleted list | |
| 180 | 3. **How it works** — 2-4 sentences | |
| 181 | 4. **Gotchas / caveats** — only if any | |
| 182 | ||
| 183 | Be concise. | |
| 184 | ||
| 185 | File: ${filePath} | |
| 186 | \`\`\` | |
| 187 | ${content.slice(0, 40000)} | |
| 188 | \`\`\``, | |
| 189 | }, | |
| 190 | ], | |
| 191 | }); | |
| 192 | return extractText(message).trim(); | |
| 193 | } |