| ... | ... | @@ -198,6 +198,7 @@ export const SHOWCASE_THREADS = [ |
| 198 | 198 | "Keep hydration errors precise, but make the development copy unexpectedly delightful.", |
| 199 | 199 | response: |
| 200 | 200 | "The diagnostics still lead with the exact mismatch and component stack. A tiny optional haiku now closes the expanded explanation.", |
| 201 | snoozeMinutes: 90, |
| 201 | 202 | }, |
| 202 | 203 | { |
| 203 | 204 | id: "beautiful-boot", |
| ... | ... | @@ -211,6 +212,17 @@ export const SHOWCASE_THREADS = [ |
| 211 | 212 | response: |
| 212 | 213 | "The plan groups milestones without changing the underlying log stream, preserves plain-text output, and adds zero work to the hot path.", |
| 213 | 214 | }, |
| 215 | { |
| 216 | id: "patient-penguins", |
| 217 | projectId: "linux", |
| 218 | title: "Teach penguins to wait patiently", |
| 219 | branch: "feat/patient-penguins", |
| 220 | minutesAgo: 52, |
| 221 | request: "Make delayed work easier to follow without adding noise to the scheduler trace.", |
| 222 | response: |
| 223 | "Delayed work now carries a concise reason through the trace, so the wait is legible without changing scheduling behavior.", |
| 224 | snoozeMinutes: 8 * 60, |
| 225 | }, |
| 214 | 226 | // Finished work, settled by hand: the list keeps it as a receded tail so |
| 215 | 227 | // the active block above reads as everything still in flight. The active |
| 216 | 228 | // block stays small enough that the settled tail begins above the fold — |
| ... | ... | @@ -337,20 +349,29 @@ function insertThread( |
| 337 | 349 | readonly minutesAgo: number; |
| 338 | 350 | readonly state?: "working" | "approval" | "plan"; |
| 339 | 351 | readonly settled?: boolean; |
| 352 | readonly snoozeMinutes?: number; |
| 340 | 353 | readonly workspaceRoot: string; |
| 341 | 354 | }, |
| 342 | 355 | ): void { |
| 343 | 356 | const turnId = `${input.id}-turn`; |
| 344 | 357 | const updatedAt = minutesBefore(now, input.minutesAgo); |
| 345 | 358 | const isWorking = input.state === "working"; |
| 359 | const snoozedUntil = |
| 360 | input.snoozeMinutes === undefined |
| 361 | ? null |
| 362 | : new Date(now + input.snoozeMinutes * 60_000).toISOString(); |
| 363 | const snoozedAt = |
| 364 | input.snoozeMinutes === undefined |
| 365 | ? null |
| 366 | : minutesBefore(now, Math.max(1, Math.floor(input.minutesAgo / 2))); |
| 346 | 367 | database |
| 347 | 368 | .prepare( |
| 348 | 369 | `INSERT INTO projection_threads ( |
| 349 | 370 | thread_id, project_id, title, model_selection_json, runtime_mode, interaction_mode, |
| 350 | 371 | branch, worktree_path, latest_turn_id, latest_user_message_at, pending_approval_count, |
| 351 | 372 | pending_user_input_count, has_actionable_proposed_plan, created_at, updated_at, |
| 352 | | archived_at, deleted_at, settled_override, settled_at |
| 353 | | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?)`, |
| 373 | archived_at, deleted_at, settled_override, settled_at, snoozed_until, snoozed_at |
| 374 | ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`, |
| 354 | 375 | ) |
| 355 | 376 | .run( |
| 356 | 377 | input.id, |
| ... | ... | @@ -369,6 +390,8 @@ function insertThread( |
| 369 | 390 | updatedAt, |
| 370 | 391 | input.settled ? "settled" : null, |
| 371 | 392 | input.settled ? updatedAt : null, |
| 393 | snoozedUntil, |
| 394 | snoozedAt, |
| 372 | 395 | ); |
| 373 | 396 | database |
| 374 | 397 | .prepare( |
| ... | ... | @@ -409,6 +432,8 @@ const SEEDED_PROJECTION_TABLES = [ |
| 409 | 432 | "projection_state", |
| 410 | 433 | ] as const; |
| 411 | 434 | |
| 435 | const SEEDED_THREAD_COLUMNS = ["snoozed_until", "snoozed_at"] as const; |
| 436 | |
| 412 | 437 | function hasSeedableSchema(dbPath: string): boolean { |
| 413 | 438 | let database: NodeSqlite.DatabaseSync; |
| 414 | 439 | try { |
| ... | ... | @@ -417,12 +442,18 @@ function hasSeedableSchema(dbPath: string): boolean { |
| 417 | 442 | return false; |
| 418 | 443 | } |
| 419 | 444 | try { |
| 420 | | const row = database |
| 445 | const tableCount = database |
| 421 | 446 | .prepare( |
| 422 | 447 | `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`, |
| 423 | 448 | ) |
| 424 | 449 | .get(...SEEDED_PROJECTION_TABLES) as { count: number }; |
| 425 | | return row.count === SEEDED_PROJECTION_TABLES.length; |
| 450 | if (tableCount.count !== SEEDED_PROJECTION_TABLES.length) return false; |
| 451 | |
| 452 | const threadColumns = database.prepare("PRAGMA table_info(projection_threads)").all() as Array<{ |
| 453 | name: string; |
| 454 | }>; |
| 455 | const threadColumnNames = new Set(threadColumns.map((column) => column.name)); |
| 456 | return SEEDED_THREAD_COLUMNS.every((column) => threadColumnNames.has(column)); |
| 426 | 457 | } catch { |
| 427 | 458 | return false; |
| 428 | 459 | } finally { |