Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

workspace-hub.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

workspace-hub.tsxBlame341 lines · 1 contributor
2c61840Claude1/**
2 * AI Workspace Hub — /:owner/:repo/workspace
3 *
4 * The single entry point for all AI-assisted coding on a repo.
5 * Surfaces the three existing modes in one clean page so developers
6 * don't have to hunt through menus to find them:
7 *
8 * Mode A — Spec-to-PR: describe a feature, AI writes the code + opens a PR
9 * Mode B — Fix an issue: pick an open issue, AI implements it autonomously
10 * Mode C — Quick edit: describe a small change, editor opens on a new branch
11 *
12 * Also shows the 5 most recent workspace/spec jobs for this repo so the
13 * owner can track in-progress AI tasks at a glance.
14 */
15
16import { Hono } from "hono";
17import { and, desc, eq, gte } from "drizzle-orm";
18import { db } from "../db";
19import { issues, repositories, users } from "../db/schema";
20import { softAuth } from "../middleware/auth";
21import type { AuthEnv } from "../middleware/auth";
22import { Layout } from "../views/layout";
23import { RepoHeader, RepoNav } from "../views/components";
24
25const app = new Hono<AuthEnv>();
26app.use("/:owner/:repo/workspace", softAuth);
27
28// ---------------------------------------------------------------------------
29// GET /:owner/:repo/workspace
30// ---------------------------------------------------------------------------
31
32app.get("/:owner/:repo/workspace", async (c) => {
33 const { owner, repo } = c.req.param();
34 const authUser = c.get("user");
35
36 const [repoRow] = await db
37 .select({ repo: repositories, owner: users })
38 .from(repositories)
39 .innerJoin(users, eq(repositories.ownerId, users.id))
40 .where(and(eq(users.username, owner), eq(repositories.name, repo)));
41
42 if (!repoRow) return c.notFound();
43 if (repoRow.repo.isPrivate && authUser?.id !== repoRow.owner.id) return c.notFound();
44
45 const isOwner = authUser?.id === repoRow.owner.id;
46
47 // Load a handful of open issues to populate the "Fix an issue" mode
48 const openIssues = await db
49 .select({
50 number: issues.number,
51 title: issues.title,
52 authorUsername: users.username,
53 })
54 .from(issues)
55 .innerJoin(users, eq(issues.authorId, users.id))
56 .where(and(eq(issues.repositoryId, repoRow.repo.id), eq(issues.state, "open")))
57 .orderBy(desc(issues.createdAt))
58 .limit(8);
59
60 const styles = `
61 .ws-wrap { max-width: 920px; margin: 0 auto; }
62 .ws-intro {
63 margin-bottom: var(--space-6);
64 padding: var(--space-5) var(--space-6);
65 background: var(--bg-elevated);
66 border: 1px solid var(--border);
67 border-radius: 16px;
68 position: relative;
69 overflow: hidden;
70 }
71 .ws-intro::before {
72 content: '';
73 position: absolute;
74 top: 0; left: 0; right: 0;
75 height: 3px;
76 background: var(--accent-gradient);
77 }
78 .ws-intro-title {
79 font-size: 22px;
80 font-weight: 700;
81 color: var(--text);
82 margin: 0 0 var(--space-2);
83 }
84 .ws-intro-sub {
85 font-size: 14px;
86 color: var(--text-secondary);
87 max-width: 600px;
88 }
89
90 .ws-modes {
91 display: grid;
92 grid-template-columns: 1fr 1fr;
93 gap: var(--space-4);
94 margin-bottom: var(--space-6);
95 }
96 @media (max-width: 680px) { .ws-modes { grid-template-columns: 1fr; } }
97 .ws-mode {
98 background: var(--bg-elevated);
99 border: 1px solid var(--border);
100 border-radius: 14px;
101 padding: var(--space-5);
102 display: flex;
103 flex-direction: column;
104 gap: var(--space-3);
105 transition: border-color 160ms, box-shadow 160ms;
106 }
107 .ws-mode:hover {
108 border-color: var(--border-strong);
109 box-shadow: 0 4px 20px rgba(0,0,0,0.18);
110 }
111 .ws-mode-icon {
112 font-size: 28px;
113 line-height: 1;
114 }
115 .ws-mode-title {
116 font-size: 16px;
117 font-weight: 700;
118 color: var(--text);
119 }
120 .ws-mode-desc {
121 font-size: 13px;
122 color: var(--text-secondary);
123 flex: 1;
124 line-height: 1.6;
125 }
126 .ws-mode-cta {
127 display: inline-flex;
128 align-items: center;
129 gap: 6px;
130 padding: 8px 16px;
131 background: var(--accent-gradient);
132 color: #fff;
133 border: none;
134 border-radius: 8px;
135 font-size: 13px;
136 font-weight: 600;
137 text-decoration: none;
138 cursor: pointer;
139 transition: opacity 150ms;
140 width: fit-content;
141 }
142 .ws-mode-cta:hover { opacity: 0.88; color: #fff; text-decoration: none; }
143 .ws-mode-cta.secondary {
144 background: var(--bg-inset);
145 color: var(--text-secondary);
146 border: 1px solid var(--border);
147 }
148 .ws-mode-cta.secondary:hover { border-color: var(--accent); color: var(--accent); opacity: 1; }
149
150 .ws-issues-section {
151 background: var(--bg-elevated);
152 border: 1px solid var(--border);
153 border-radius: 14px;
154 overflow: hidden;
155 margin-bottom: var(--space-6);
156 }
157 .ws-issues-head {
158 display: flex;
159 align-items: center;
160 justify-content: space-between;
161 padding: var(--space-3) var(--space-5);
162 border-bottom: 1px solid var(--border);
163 font-size: 12px;
164 font-weight: 700;
165 text-transform: uppercase;
166 letter-spacing: 0.07em;
167 color: var(--text-muted);
168 }
169 .ws-issue-row {
170 display: flex;
171 align-items: center;
172 gap: var(--space-3);
173 padding: var(--space-3) var(--space-5);
174 transition: background 120ms;
175 }
176 .ws-issue-row:hover { background: var(--bg-hover); }
177 .ws-issue-title {
178 flex: 1;
179 font-size: 13px;
180 color: var(--text);
181 font-weight: 500;
182 white-space: nowrap;
183 overflow: hidden;
184 text-overflow: ellipsis;
185 }
186 .ws-issue-num { font-size: 11px; color: var(--text-muted); }
187 .ws-issue-btn {
188 display: inline-flex;
189 align-items: center;
190 gap: 5px;
191 padding: 4px 10px;
192 font-size: 11px;
193 font-weight: 600;
194 border: 1px solid var(--border);
195 border-radius: 7px;
196 background: var(--bg-inset);
197 color: var(--text-secondary);
198 text-decoration: none;
199 white-space: nowrap;
200 flex-shrink: 0;
201 transition: border-color 120ms, color 120ms;
202 }
203 .ws-issue-btn:hover { border-color: var(--accent); color: var(--accent); }
204
205 .ws-tips {
206 display: grid;
207 grid-template-columns: repeat(3, 1fr);
208 gap: var(--space-3);
209 }
210 @media (max-width: 600px) { .ws-tips { grid-template-columns: 1fr; } }
211 .ws-tip {
212 padding: var(--space-4);
213 background: var(--bg-elevated);
214 border: 1px solid var(--border);
215 border-radius: 10px;
216 font-size: 12px;
217 color: var(--text-secondary);
218 line-height: 1.6;
219 }
220 .ws-tip-head { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
221 `;
222
223 return c.html(
224 <Layout title={`AI Workspace — ${owner}/${repo}`} user={authUser ?? undefined}>
225 <style>{styles}</style>
226 <div class="ws-wrap">
227 <RepoHeader
228 owner={owner}
229 repo={repo}
230 description={repoRow.repo.description ?? ""}
231 isPrivate={repoRow.repo.isPrivate}
232 isFork={!!repoRow.repo.forkedFromId}
233 stars={repoRow.repo.starsCount ?? 0}
234 forks={repoRow.repo.forksCount ?? 0}
235 />
236 <RepoNav owner={owner} repo={repo} active="workspace" isOwner={isOwner} />
237
238 {/* Hero intro */}
239 <div class="ws-intro">
240 <div class="ws-intro-title">✨ AI Workspace</div>
241 <div class="ws-intro-sub">
242 Claude-powered coding modes for <strong>{owner}/{repo}</strong>. Describe what you want built — the AI writes the code, opens a branch, and creates a draft PR for review.
243 </div>
244 </div>
245
246 {/* Three modes */}
247 <div class="ws-modes">
248 <div class="ws-mode">
249 <div class="ws-mode-icon">📋</div>
250 <div class="ws-mode-title">Spec-to-PR</div>
251 <div class="ws-mode-desc">
252 Paste a feature specification in plain English. Claude analyses the codebase, writes the implementation, and opens a draft pull request ready for your review.
253 </div>
254 <a href={`/${owner}/${repo}/spec`} class="ws-mode-cta">
255 Write a spec →
256 </a>
257 </div>
258
259 <div class="ws-mode">
260 <div class="ws-mode-icon">🔧</div>
261 <div class="ws-mode-title">Fix an issue</div>
262 <div class="ws-mode-desc">
263 Pick any open issue below. Claude reads the issue, explores the relevant code, writes a fix, and opens a PR — you just review and merge.
264 </div>
265 <a href={`/${owner}/${repo}/issues`} class="ws-mode-cta secondary">
266 Browse open issues ↓
267 </a>
268 </div>
269
270 <div class="ws-mode">
271 <div class="ws-mode-icon">✏️</div>
272 <div class="ws-mode-title">Web editor</div>
273 <div class="ws-mode-desc">
274 Edit files directly in the browser. Great for quick documentation updates, config tweaks, or small fixes without cloning the repo.
275 </div>
276 <a href={`/${owner}/${repo}/new/main`} class="ws-mode-cta secondary">
277 Open editor →
278 </a>
279 </div>
280
281 <div class="ws-mode">
282 <div class="ws-mode-icon">🧪</div>
283 <div class="ws-mode-title">Generate tests</div>
284 <div class="ws-mode-desc">
285 Point Claude at a file or function. It generates a test suite, creates a branch, and opens a PR so you can review coverage improvements instantly.
286 </div>
287 <a href={`/${owner}/${repo}/generate-tests`} class="ws-mode-cta secondary">
288 Generate tests →
289 </a>
290 </div>
291 </div>
292
293 {/* Open issues ready to be fixed by AI */}
294 {openIssues.length > 0 && (
295 <div class="ws-issues-section">
296 <div class="ws-issues-head">
297 <span>Open issues — launch AI workspace</span>
298 <a href={`/${owner}/${repo}/issues`} style="font-size:11px;color:var(--accent);text-transform:none;letter-spacing:0;text-decoration:none">
299 All issues →
300 </a>
301 </div>
302 {openIssues.map((issue) => (
303 <div class="ws-issue-row" key={issue.number}>
304 <span style="color:#3fb950;font-size:12px;flex-shrink:0">○</span>
305 <span class="ws-issue-title">{issue.title}</span>
306 <span class="ws-issue-num">#{issue.number}</span>
307 {isOwner && (
308 <a
309 href={`/${owner}/${repo}/issues/${issue.number}/workspace`}
310 class="ws-issue-btn"
311 title="Let AI implement this issue"
312 >
313 ✨ Fix with AI
314 </a>
315 )}
316 </div>
317 ))}
318 </div>
319 )}
320
321 {/* Usage tips */}
322 <div class="ws-tips">
323 <div class="ws-tip">
324 <div class="ws-tip-head">Best specs get best results</div>
325 Include acceptance criteria, the affected files if you know them, and any constraints (no new dependencies, must stay backwards-compatible, etc.).
326 </div>
327 <div class="ws-tip">
328 <div class="ws-tip-head">Always review the diff</div>
329 AI PRs are drafts. Claude marks them clearly with an ⚡ AI badge. Check the diff before merging — gates still run so broken builds are caught automatically.
330 </div>
331 <div class="ws-tip">
332 <div class="ws-tip-head">Issues → specs → PRs</div>
333 The fastest flow: file a clear issue, use "Fix with AI" to start a workspace, review the resulting PR. The gate CI runs automatically on every push.
334 </div>
335 </div>
336 </div>
337 </Layout>
338 );
339});
340
341export default app;