Add real-time filters, Vercel auto-detection, and fix prompt extraction

jasonnovack@jasonnovackJan 7, 2026claude_codeclaude-opus-4-5-20251101featurevercelfiltersux

Diff

diff --git a/packages/cli/src/commands/submit.ts b/packages/cli/src/commands/submit.ts
index a2bbba5..fadea18 100644
--- a/packages/cli/src/commands/submit.ts
+++ b/packages/cli/src/commands/submit.ts
@@ -1,6 +1,7 @@
import { simpleGit } from 'simple-git'
import { detectHarness, type ExtractedSession } from '../extractors/index.js'
import { loadConfig } from './login.js'
+import { detectVercelDeployments, loadVercelConfig } from './vercel.js'
import * as readline from 'readline'
interface SubmitOptions {
@@ -9,6 +10,8 @@ interface SubmitOptions {
type: string
tags?: string
apiUrl: string
+ beforePreviewUrl?: string
+ afterPreviewUrl?: string
}
async function prompt(question: string): Promise<string> {
@@ -44,17 +47,48 @@ export async function submit(options: SubmitOptions) {
process.exit(1)
}
- // Get git info
+ // Detect harness and extract session FIRST to get timing info
+ console.log('šŸ” Detecting AI harness...')
+
+ let session: ExtractedSession | null = null
+
+ // Try auto-detection with optional harness preference
+ session = await detectHarness(cwd, options.harness)
+
+ // Get git info - use session timestamp to find correct "before" commit
console.log('šŸ“¦ Reading git state...')
- const log = await git.log({ maxCount: 2 })
+ // Get more commits to find the one before the session started
+ const log = await git.log({ maxCount: 50 })
if (log.all.length < 2) {
- console.error('āŒ Need at least 2 commits. BEFORE = HEAD~1, AFTER = HEAD.')
+ console.error('āŒ Need at least 2 commits. BEFORE = commit before session, AFTER = HEAD.')
process.exit(1)
}
const afterCommit = log.all[0]
- const beforeCommit = log.all[1]
+
+ // Find the "before" commit - the most recent commit BEFORE the session started
+ let beforeCommit = log.all[1] // Default to HEAD~1
+ if (session?.timestamp) {
+ const sessionTime = session.timestamp.getTime()
+ // Find the first commit that predates the session
+ for (const commit of log.all) {
+ const commitTime = new Date(commit.date).getTime()
+ if (commitTime < sessionTime) {
+ beforeCommit = commit
+ break
+ }
+ }
+ if (beforeCommit === log.all[1] && log.all.length > 2) {
+ // Check if we found a better match
+ const foundBetterMatch = log.all.some((commit, i) =>
+ i > 1 && new Date(commit.date).getTime() < sessionTime
+ )
+ if (foundBetterMatch) {
+ console.log(` Session started: ${session.timestamp.toISOString()}`)
+ }
+ }
+ }
console.log(` BEFORE: ${beforeCommit.hash.slice(0, 7)} - ${beforeCommit.message.split('\n')[0]}`)
console.log(` AFTER: ${afterCommit.hash.slice(0, 7)} - ${afterCommit.message.split('\n')[0]}`)
@@ -87,14 +121,7 @@ export async function submit(options: SubmitOptions) {
repoUrl = await prompt('šŸ”— Repo URL (GitHub, etc.): ')
}
- // Detect harness and extract session
- console.log('šŸ” Detecting AI harness...')
-
- let session: ExtractedSession | null = null
-
- // Try auto-detection with optional harness preference
- session = await detectHarness(cwd, options.harness)
-
+ // Print session info if found
if (session) {
console.log(` āœ“ Found ${harnessNames[session.harness] || session.harness} session`)
console.log(` Model: ${session.model}`)
@@ -131,6 +158,35 @@ export async function submit(options: SubmitOptions) {
// Parse tags
const tags = options.tags ? options.tags.split(',').map(t => t.trim()) : []
+ // Auto-detect Vercel deployment URLs (if Vercel is connected)
+ let beforePreviewUrl = options.beforePreviewUrl
+ let afterPreviewUrl = options.afterPreviewUrl
+
+ if (!beforePreviewUrl || !afterPreviewUrl) {
+ const vercelConfig = loadVercelConfig()
+ if (vercelConfig) {
+ console.log('\nšŸ” Detecting Vercel deployments...')
+ const deployments = await detectVercelDeployments(
+ repoUrl,
+ beforeCommit.hash,
+ afterCommit.hash
+ )
+
+ if (deployments.beforeUrl || deployments.afterUrl) {
+ if (deployments.beforeUrl && !beforePreviewUrl) {
+ beforePreviewUrl = deployments.beforeUrl
+ console.log(` āœ“ Before: ${beforePreviewUrl}`)
+ }
+ if (deployments.afterUrl && !afterPreviewUrl) {
+ afterPreviewUrl = deployments.afterUrl
+ console.log(` āœ“ After: ${afterPreviewUrl}`)
+ }
+ } else {
+ console.log(' āš ļø No Vercel deployments found for these commits')
+ }
+ }
+ }
+
// Prepare payload with enhanced session data
const enhancedSessionData = {
...session.sessionData,
@@ -150,6 +206,8 @@ export async function submit(options: SubmitOptions) {
beforeCommitHash: beforeCommit.hash,
afterCommitHash: afterCommit.hash,
diff,
+ beforePreviewUrl,
+ afterPreviewUrl,
harness: session.harness,
model: session.model,
prompt: session.prompt,
diff --git a/packages/cli/src/commands/vercel.ts b/packages/cli/src/commands/vercel.ts
new file mode 100644
index 0000000..40d7261
--- /dev/null
+++ b/packages/cli/src/commands/vercel.ts
@@ -0,0 +1,227 @@
+import * as fs from 'fs'
+import * as path from 'path'
+import * as os from 'os'
+import * as readline from 'readline'
+
+interface VercelConfig {
+ token: string
+}
+
+interface VercelDeployment {
+ uid: string
+ url: string
+ state: string
+ meta?: {
+ githubCommitSha?: string
+ gitCommitSha?: string
+ }
+}
+
+interface VercelProject {
+ id: string
+ name: string
+ link?: {
+ type: string
+ repo: string
+ repoId: number
+ }
+}
+
+const CONFIG_PATH = path.join(os.homedir(), '.oneshot', 'vercel.json')
+
+function prompt(question: string): Promise<string> {
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ })
+
+ return new Promise((resolve) => {
+ rl.question(question, (answer) => {
+ rl.close()
+ resolve(answer)
+ })
+ })
+}
+
+export function loadVercelConfig(): VercelConfig | null {
+ try {
+ if (fs.existsSync(CONFIG_PATH)) {
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
+ }
+ } catch {
+ // Config doesn't exist or is invalid
+ }
+ return null
+}
+
+function saveVercelConfig(config: VercelConfig) {
+ const dir = path.dirname(CONFIG_PATH)
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true })
+ }
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
+}
+
+/**
+ * Login to Vercel - stores API token
+ */
+export async function vercelLogin() {
+ console.log('\nšŸ”— Vercel Integration Setup\n')
+ console.log('To automatically detect Vercel deployment URLs, you need a Vercel API token.')
+ console.log('')
+ console.log('1. Go to: https://vercel.com/account/tokens')
+ console.log('2. Create a new token with "Read" scope')
+ console.log('3. Copy and paste it below\n')
+
+ const token = await prompt('Vercel API Token: ')
+
+ if (!token.trim()) {
+ console.error('āŒ No token provided')
+ process.exit(1)
+ }
+
+ // Verify the token works
+ console.log('\nšŸ” Verifying token...')
+ try {
+ const response = await fetch('https://api.vercel.com/v2/user', {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+
+ if (!response.ok) {
+ console.error('āŒ Invalid token or API error')
+ process.exit(1)
+ }
+
+ const user = await response.json()
+ console.log(`āœ… Authenticated as: ${user.user.username || user.user.email}`)
+
+ saveVercelConfig({ token })
+ console.log('\nāœ“ Vercel token saved. Deployment URLs will be auto-detected on submit.\n')
+ } catch (error) {
+ console.error(`āŒ Failed to verify token: ${error}`)
+ process.exit(1)
+ }
+}
+
+/**
+ * Logout from Vercel
+ */
+export function vercelLogout() {
+ if (fs.existsSync(CONFIG_PATH)) {
+ fs.unlinkSync(CONFIG_PATH)
+ console.log('āœ“ Vercel token removed')
+ } else {
+ console.log('Not logged in to Vercel')
+ }
+}
+
+/**
+ * Find Vercel project for a GitHub repo
+ */
+async function findVercelProject(token: string, repoUrl: string): Promise<VercelProject | null> {
+ // Extract owner/repo from GitHub URL
+ const match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/)
+ if (!match) return null
+
+ const [, owner, repo] = match
+ const repoFullName = `${owner}/${repo}`
+
+ try {
+ // List all projects and find one linked to this repo
+ const response = await fetch('https://api.vercel.com/v9/projects?limit=100', {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+
+ if (!response.ok) return null
+
+ const data = await response.json()
+ const projects = data.projects as VercelProject[]
+
+ // Find project linked to this GitHub repo
+ for (const project of projects) {
+ if (project.link?.type === 'github') {
+ // The repo field might be just repo name or owner/repo
+ const linkedRepo = project.link.repo
+ if (linkedRepo === repoFullName || linkedRepo === repo) {
+ return project
+ }
+ }
+ }
+ } catch {
+ // API error
+ }
+
+ return null
+}
+
+/**
+ * Find deployment URL for a specific commit
+ */
+async function findDeploymentForCommit(
+ token: string,
+ projectId: string,
+ commitSha: string
+): Promise<string | null> {
+ try {
+ // Query deployments for this project, filter by commit
+ const response = await fetch(
+ `https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=100`,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }
+ )
+
+ if (!response.ok) return null
+
+ const data = await response.json()
+ const deployments = data.deployments as VercelDeployment[]
+
+ // Find deployment matching this commit SHA
+ for (const deployment of deployments) {
+ const deployCommit = deployment.meta?.githubCommitSha || deployment.meta?.gitCommitSha
+ if (deployCommit === commitSha && deployment.state === 'READY') {
+ // Return the deployment URL (add https:// if needed)
+ const url = deployment.url
+ return url.startsWith('http') ? url : `https://${url}`
+ }
+ }
+ } catch {
+ // API error
+ }
+
+ return null
+}
+
+/**
+ * Auto-detect Vercel deployment URLs for before/after commits
+ */
+export async function detectVercelDeployments(
+ repoUrl: string,
+ beforeCommitSha: string,
+ afterCommitSha: string
+): Promise<{ beforeUrl: string | null; afterUrl: string | null }> {
+ const config = loadVercelConfig()
+ if (!config?.token) {
+ return { beforeUrl: null, afterUrl: null }
+ }
+
+ // Find the Vercel project for this repo
+ const project = await findVercelProject(config.token, repoUrl)
+ if (!project) {
+ return { beforeUrl: null, afterUrl: null }
+ }
+
+ // Find deployments for both commits
+ const [beforeUrl, afterUrl] = await Promise.all([
+ findDeploymentForCommit(config.token, project.id, beforeCommitSha),
+ findDeploymentForCommit(config.token, project.id, afterCommitSha),
+ ])
+
+ return { beforeUrl, afterUrl }
+}
diff --git a/packages/cli/src/extractors/claude-code.ts b/packages/cli/src/extractors/claude-code.ts
index 887629d..36c7f44 100644
--- a/packages/cli/src/extractors/claude-code.ts
+++ b/packages/cli/src/extractors/claude-code.ts
@@ -354,15 +354,28 @@ export async function extractSession(sessionPath: string, projectPath: string):
const msg = JSON.parse(line) as ClaudeMessage
messages.push(msg)
- // Extract user prompts - handle both direct and nested message structure
- const msgRole = msg.role || msg.message?.role
- const msgContent = msg.content || msg.message?.content
-
- if (msg.type === 'user' || msgRole === 'user') {
- const text = extractText(msgContent)
- // Keep the longest user prompt (most likely to be the main instruction)
- // Short messages like "yes", "continue", "planning mode" are likely not the main prompt
- if (text && text.length > userPrompt.length) {
+ // Extract user prompts - be strict about what constitutes a user message
+ // Only accept messages that are explicitly marked as user type
+ // and don't have assistant-like characteristics
+ const isUserMessage = msg.type === 'user' && msg.role !== 'assistant'
+
+ if (isUserMessage) {
+ // For user messages, extract only the direct content (not nested message content)
+ // as nested content might be assistant responses
+ const text = extractText(msg.content)
+
+ // Skip short confirmation messages and common AI-interaction phrases
+ const skipPhrases = [
+ 'yes', 'no', 'ok', 'okay', 'continue', 'planning mode',
+ 'proceed', 'go ahead', 'sure', 'thanks', 'thank you'
+ ]
+ const normalizedText = text.toLowerCase().trim()
+ const isSkippable = skipPhrases.some(phrase =>
+ normalizedText === phrase || normalizedText === phrase + '.'
+ )
+
+ // Keep the longest non-skippable user prompt
+ if (text && text.length > userPrompt.length && !isSkippable) {
userPrompt = text
}
}
diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts
index fdeda49..ea250ba 100644
--- a/packages/cli/src/index.ts
+++ b/packages/cli/src/index.ts
@@ -3,6 +3,7 @@
import { Command } from 'commander'
import { submit } from './commands/submit.js'
import { login, logout, whoami } from './commands/login.js'
+import { vercelLogin, vercelLogout } from './commands/vercel.js'
const program = new Command()
@@ -34,7 +35,19 @@ program
.option('--title <title>', 'Shot title')
.option('--type <type>', 'Transformation type (feature, fix, refactor, ui, test, docs, other)', 'feature')
.option('--tags <tags>', 'Comma-separated tags')
+ .option('--before-preview-url <url>', 'Override auto-detected before preview URL')
+ .option('--after-preview-url <url>', 'Override auto-detected after preview URL')
.option('--api-url <url>', 'API base URL', 'http://localhost:3000')
.action(submit)
+program
+ .command('vercel-login')
+ .description('Connect Vercel for automatic deployment URL detection')
+ .action(vercelLogin)
+
+program
+ .command('vercel-logout')
+ .description('Disconnect Vercel integration')
+ .action(vercelLogout)
+
program.parse()
diff --git a/packages/web/src/app/api/shots/route.ts b/packages/web/src/app/api/shots/route.ts
index 51ef0e9..81f4877 100644
--- a/packages/web/src/app/api/shots/route.ts
+++ b/packages/web/src/app/api/shots/route.ts
@@ -81,6 +81,8 @@ export async function POST(request: NextRequest) {
beforeCommitHash: body.beforeCommitHash,
afterCommitHash: body.afterCommitHash,
diff: body.diff,
+ beforePreviewUrl: body.beforePreviewUrl || null,
+ afterPreviewUrl: body.afterPreviewUrl || null,
harness: body.harness,
model: body.model,
prompt: body.prompt,
diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css
index 9b0782d..f145fd5 100644
--- a/packages/web/src/app/globals.css
+++ b/packages/web/src/app/globals.css
@@ -696,3 +696,32 @@ h1 {
.social-link:hover {
color: var(--accent);
}
+
+/* Shot date */
+.shot-date {
+ color: var(--muted);
+ font-size: 0.8rem;
+}
+
+/* Preview links */
+.preview-links {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.preview-link {
+ color: #4ade80;
+ text-decoration: none;
+ font-weight: 500;
+ padding: 0.25rem 0.5rem;
+ background: rgba(74, 222, 128, 0.1);
+ border-radius: 4px;
+ font-size: 0.875rem;
+}
+
+.preview-link:hover {
+ background: rgba(74, 222, 128, 0.2);
+ text-decoration: none;
+}
diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx
index 2408a9d..526d7a0 100644
--- a/packages/web/src/app/page.tsx
+++ b/packages/web/src/app/page.tsx
@@ -3,6 +3,7 @@ import { shots, users } from '@/db/schema'
import { desc, eq, ilike, or, and, sql } from 'drizzle-orm'
import Link from 'next/link'
import { unstable_noStore as noStore } from 'next/cache'
+import { GalleryFilters } from '@/components/GalleryFilters'
export const dynamic = 'force-dynamic'
export const revalidate = 0
@@ -125,44 +126,10 @@ export default async function GalleryPage({ searchParams }: Props) {
)}
{/* Filters */}
- <form className="filters" method="GET">
- <input
- type="text"
- name="q"
- placeholder="Search shots..."
- defaultValue={q || ''}
- className="search-input"
- />
- <select name="harness" defaultValue={harness || ''} className="filter-select">
- <option value="">All harnesses</option>
- {harnessOptions.map((h) => (
- <option key={h.harness} value={h.harness}>{h.harness}</option>
- ))}
- </select>
- <select name="type" defaultValue={type || ''} className="filter-select">
- <option value="">All types</option>
- {typeOptions.map((t) => (
- <option key={t} value={t}>{t}</option>
- ))}
- </select>
- <input
- type="text"
- name="model"
- placeholder="Model..."
- defaultValue={model || ''}
- className="search-input"
- style={{ width: '150px' }}
- />
- <select name="sort" defaultValue={sort} className="filter-select">
- <option value="newest">Newest</option>
- <option value="stars">Most Starred</option>
- <option value="comments">Most Discussed</option>
- </select>
- <button type="submit" className="filter-btn">Filter</button>
- {(q || harness || model || type) && (
- <Link href="/" className="clear-btn">Clear</Link>
- )}
- </form>
+ <GalleryFilters
+ harnessOptions={harnessOptions.map(h => h.harness)}
+ typeOptions={typeOptions}
+ />
{allShots.length === 0 ? (
<p style={{ color: 'var(--muted)', marginTop: '2rem' }}>
@@ -182,6 +149,13 @@ export default async function GalleryPage({ searchParams }: Props) {
@{user.username}
</Link>
)}
+ <span className="shot-date">
+ {new Date(shot.createdAt).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: shot.createdAt.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
+ })}
+ </span>
<span className="badge">{shot.harness}</span>
<span className="badge">{shot.model}</span>
<span className="badge">{shot.type}</span>
diff --git a/packages/web/src/app/shots/[id]/page.tsx b/packages/web/src/app/shots/[id]/page.tsx
index 920a883..ce9c46c 100644
--- a/packages/web/src/app/shots/[id]/page.tsx
+++ b/packages/web/src/app/shots/[id]/page.tsx
@@ -75,8 +75,37 @@ export default async function ShotDetailPage({ params }: Props) {
</div>
<div className="shot-info">
+ {/* Live Preview Links */}
+ {(shot.beforePreviewUrl || shot.afterPreviewUrl) && (
+ <div className="preview-links">
+ <strong>Live Preview:</strong>{' '}
+ {shot.beforePreviewUrl && (
+ <a
+ href={shot.beforePreviewUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="preview-link"
+ >
+ Before ↗
+ </a>
+ )}
+ {shot.beforePreviewUrl && shot.afterPreviewUrl && (
+ <span className="separator">→</span>
+ )}
+ {shot.afterPreviewUrl && (
+ <a
+ href={shot.afterPreviewUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="preview-link"
+ >
+ After ↗
+ </a>
+ )}
+ </div>
+ )}
<div className="commit-links">
- <strong>Before:</strong>{' '}
+ <strong>Commits:</strong>{' '}
<a
href={`${shot.repoUrl}/tree/${shot.beforeCommitHash}`}
target="_blank"
@@ -86,7 +115,6 @@ export default async function ShotDetailPage({ params }: Props) {
<code>{shot.beforeCommitHash.slice(0, 7)}</code>
</a>
<span className="separator">→</span>
- <strong>After:</strong>{' '}
<a
href={`${shot.repoUrl}/tree/${shot.afterCommitHash}`}
target="_blank"
@@ -102,7 +130,7 @@ export default async function ShotDetailPage({ params }: Props) {
rel="noopener noreferrer"
className="github-diff-link"
>
- View on GitHub ↗
+ View diff on GitHub ↗
</a>
</div>
<div>
diff --git a/packages/web/src/components/GalleryFilters.tsx b/packages/web/src/components/GalleryFilters.tsx
new file mode 100644
index 0000000..d9f468f
--- /dev/null
+++ b/packages/web/src/components/GalleryFilters.tsx
@@ -0,0 +1,94 @@
+'use client'
+
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useCallback } from 'react'
+import Link from 'next/link'
+
+interface Props {
+ harnessOptions: string[]
+ typeOptions: string[]
+}
+
+export function GalleryFilters({ harnessOptions, typeOptions }: Props) {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+
+ const q = searchParams.get('q') || ''
+ const harness = searchParams.get('harness') || ''
+ const model = searchParams.get('model') || ''
+ const type = searchParams.get('type') || ''
+ const sort = searchParams.get('sort') || 'newest'
+
+ const hasFilters = q || harness || model || type
+
+ const updateParams = useCallback((key: string, value: string) => {
+ const params = new URLSearchParams(searchParams.toString())
+ if (value) {
+ params.set(key, value)
+ } else {
+ params.delete(key)
+ }
+ router.push(`/?${params.toString()}`)
+ }, [router, searchParams])
+
+ // Debounce text input updates
+ const handleTextChange = useCallback((key: string, value: string) => {
+ // Use a short delay for text inputs to avoid too many navigations
+ const timeoutId = setTimeout(() => {
+ updateParams(key, value)
+ }, 300)
+ return () => clearTimeout(timeoutId)
+ }, [updateParams])
+
+ return (
+ <div className="filters">
+ <input
+ type="text"
+ placeholder="Search shots..."
+ defaultValue={q}
+ className="search-input"
+ onChange={(e) => handleTextChange('q', e.target.value)}
+ />
+ <select
+ value={harness}
+ className="filter-select"
+ onChange={(e) => updateParams('harness', e.target.value)}
+ >
+ <option value="">All harnesses</option>
+ {harnessOptions.map((h) => (
+ <option key={h} value={h}>{h}</option>
+ ))}
+ </select>
+ <select
+ value={type}
+ className="filter-select"
+ onChange={(e) => updateParams('type', e.target.value)}
+ >
+ <option value="">All types</option>
+ {typeOptions.map((t) => (
+ <option key={t} value={t}>{t}</option>
+ ))}
+ </select>
+ <input
+ type="text"
+ placeholder="Model..."
+ defaultValue={model}
+ className="search-input"
+ style={{ width: '150px' }}
+ onChange={(e) => handleTextChange('model', e.target.value)}
+ />
+ <select
+ value={sort}
+ className="filter-select"
+ onChange={(e) => updateParams('sort', e.target.value)}
+ >
+ <option value="newest">Newest</option>
+ <option value="stars">Most Starred</option>
+ <option value="comments">Most Discussed</option>
+ </select>
+ {hasFilters && (
+ <Link href="/" className="clear-btn">Clear</Link>
+ )}
+ </div>
+ )
+}
diff --git a/packages/web/src/db/schema.ts b/packages/web/src/db/schema.ts
index bc1624b..7c24dee 100644
--- a/packages/web/src/db/schema.ts
+++ b/packages/web/src/db/schema.ts
@@ -30,6 +30,10 @@ export const shots = pgTable('shots', {
afterCommitHash: text('after_commit_hash').notNull(),
diff: text('diff').notNull(),
+ // Preview URLs (optional - for hosted live demos)
+ beforePreviewUrl: text('before_preview_url'),
+ afterPreviewUrl: text('after_preview_url'),
+
// AI action
harness: text('harness').notNull(), // claude_code | cursor | codex
model: text('model').notNull(),

Recipe

Model
claude-opus-4-5-20251101
Harness
Claude Code
Prompt
1. shots still don't identify the submitter in the frontend. 2. shots should also show when they were submitted. 3. let's change the gallery search & filters to be real-time upon selection instead of requiring the user to click Filter. 4. the detected prompt is wrong. it seems to show the whole convo including a bunch of text generated by the AI, instead of the user's actual prompt. 5. linking to the before and after commits is nice but remember I really want to link to the live usable app before and after, I guess by grabbing the links to 'Visit' the corresponding Vercel deployment. 6. i can see now that sometimes a single prompt may trigger the AI to do a lot of work that includes multiple commits, so we can't assume that the relevant 'before' is the immediate prior one on GitHub, instead we need to find the one immediately before the relevant user prompt, which may be multiple commits in the past.
Tip: Copy the prompt and adapt it for your own project. The key is understanding why this prompt worked, not reproducing it exactly.

Comments (0)

Loading comments...