Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
fc46e49
docs: native chat redesign design spec
Jul 27, 2026
627ee09
docs: native chat redesign implementation plan
Jul 27, 2026
5396c69
feat(chat): add ChatMode message-shaping enum
Jul 27, 2026
98e65eb
feat(chat): add reusable ChatComposer view
Jul 27, 2026
81ef327
feat(chat): add ChatEmptyState centered hero view
Jul 27, 2026
8ad9f9f
feat(chat): wire composer + empty state, drop old input/greeting/lets…
Jul 27, 2026
f9731a9
style(chat): restyle bubbles + drop dead founderName/redundant frame
Jul 27, 2026
8912907
fix(chat): refocus composer after send + elevate with floatingShadow
Jul 27, 2026
df83ef8
fix(chat): drop stray menu disclosure chevron in composer
Jul 27, 2026
585aa18
feat(chat): v2 polish — companion orb, glossy composer, message actio…
Jul 27, 2026
26cab11
docs: chat-as-main-interface spec + plan
Jul 27, 2026
6f3d4f1
feat(nav): extract RoadmapView + SecondBrainView from Overview
Jul 27, 2026
ea9f701
feat(nav): chat is the full-width home; retire Overview; Roadmap/Seco…
Jul 27, 2026
ca4a1e3
style(chat): center chat content in a max-width column for full-width
Jul 27, 2026
9e35a6f
feat(chat): personalized greeting + purple-gradient brand polish + qu…
Jul 27, 2026
194708f
docs: sidebar restructure plan
Jul 27, 2026
e4bc851
feat(nav): add SidebarView (brand, New chat, Recent, Workspace, account)
Jul 27, 2026
8a6a44b
feat(nav): replace top bar with SidebarView
Jul 27, 2026
122faf1
feat(chat): drop Your-team header + in-chat History (now in sidebar)
Jul 27, 2026
7cd1be6
docs: session handoff for the chat-redesign branch
Jul 27, 2026
775d0ab
refactor(shell): clear stale chrome from the sidebar restructure
Jul 27, 2026
5401225
docs: chat containment + cohesion design spec
Jul 27, 2026
5d01a41
docs: chat containment + cohesion implementation plan
Jul 27, 2026
57e34e6
feat(chat): share one ambient backdrop behind empty + active
Jul 27, 2026
25bb22d
fix(chat): contain the docked composer in the 720 column, drop divider
Jul 27, 2026
a3740ae
fix(chat): match messageList's HStack(spacing: 0) so columns align
Jul 27, 2026
5e328d3
docs(chat): drop stale "brand wash" from ChatEmptyState doc comment
Jul 27, 2026
2525694
fix(chat): narrow the shared chat column 720 -> 600 so it reads as co…
Jul 28, 2026
9c26d73
fix(sidebar): drop the gradient mark from the brand row, keep the wor…
Jul 28, 2026
c24067d
docs: luminous orb + streaming affordance design spec
Jul 28, 2026
6dfeea6
docs: luminous orb + streaming affordance implementation plan
Jul 28, 2026
504b74c
feat(chat): per-companion second hue + chatCanvas/chatOrbCore tokens
Jul 28, 2026
a96c725
feat(chat): pure ChatThinkingLabel (names the work, honest fallback)
Jul 28, 2026
6cebe02
feat(chat): luminous companion-tinted orb with working-breathe
Jul 28, 2026
0637368
feat(chat): ChatThinkingRow replaces static typing/producing text
Jul 28, 2026
84cccb2
fix(sidebar): Recent hugs its threads instead of reserving 260pt
Jul 28, 2026
f069f9b
docs: message card grammar + chat readability design spec
Jul 28, 2026
767fcb4
docs: message card grammar + readability implementation plan
Jul 28, 2026
cd0ec1f
feat(chat): pure MessageCardStyle (kind + six semantic hues)
Jul 28, 2026
70c0ce6
feat(chat): one tinted card grammar for all payloads + Inter reading …
Jul 28, 2026
840cca7
fix(chat): paint opaque surface under MessageCard's hue tint
Jul 28, 2026
5dab7be
docs: message rendering (un-bubble) + per-message feedback design spec
Jul 28, 2026
6f0d2d2
docs: message rendering + per-message feedback implementation plan
Jul 28, 2026
8d414a4
feat(chat): pure MessageFeedback payload for per-message reactions
Jul 28, 2026
62dd726
feat(chat): un-bubble assistant messages + per-message thumbs
Jul 28, 2026
cb0adc7
docs: composer department chips + state expression design spec
Jul 28, 2026
b709240
docs: composer chips + state expression implementation plan
Jul 28, 2026
fffeae9
feat(chat): department-focus grounding scope (real dept chips backend)
Jul 28, 2026
322e5dd
feat(chat): composer department chips + focus/busy states + tinted send
Jul 28, 2026
80060e1
docs: first-run/empty chat state — live landing design spec
Jul 28, 2026
f1194dc
docs: first-run/empty live landing implementation plan
Jul 28, 2026
dce9fd2
feat(chat): pure ChatLandingState (greeting + live roadmap signals)
Jul 28, 2026
b719ef2
test(chat): fixture proves needsYouCount excludes the beacon
Jul 28, 2026
e68ffcd
feat(chat): live landing cards (beacon/needs-you/awaiting) + starters
Jul 28, 2026
dfd394c
docs: reconciliation execution guide (redesign onto post-#37 main)
Jul 28, 2026
4aabb74
Merge remote-tracking branch 'origin/main' into reconcile/redesign-on…
Jul 28, 2026
a4a9c05
feat(chat): DEBUG-only mock chat seam for creditless UI testing
Jul 28, 2026
6b4f40a
feat(chat): keyword-routed mock harness for full offline chat test
Jul 28, 2026
8a7e6e5
fix(chat): breathing room above the transcript
Jul 28, 2026
36daf7e
feat(chat): canned roadmap in mock so the run loop is testable offline
Jul 28, 2026
dbdd8fa
fix(chat): readable draft-card preview in dark mode
Jul 28, 2026
56bbf47
feat(chat): conversational enrichment question (drop the pinned box)
Jul 28, 2026
de1cbf9
feat(chat): department -> companion handoff (#3)
Jul 28, 2026
d863599
fix(onboarding): size the flow proportionally so full-screen holds up
Jul 28, 2026
0cf3d0b
feat(chat): streaming execute-log during a run (#2)
Jul 28, 2026
712afb4
fix(chat): execute-log animates sequentially (pacing)
Jul 28, 2026
03fceae
fix(onboarding): drop the art panel's upper clamp — it recreated the bug
Jul 28, 2026
71dc918
feat(chat): web-style execute-log + pet-sprite specialists
Jul 28, 2026
1de9e26
fix(chat): every run shows the execute-log (unify run paths)
Jul 28, 2026
d5bd15d
feat(onboarding): port the web's motion layer to splash + onboarding
Jul 28, 2026
606b60e
fix(chat): widen chat column + composer to 760 (match Claude Code)
Jul 28, 2026
420491e
feat(chat): thread header — name dropdown switcher + Share
Jul 28, 2026
b9aa9e3
fix(onboarding): drop the pointer parallax on the onboarding images
Jul 28, 2026
873df12
feat(theme): port --on-accent luminance-picked text on accent fills
Jul 28, 2026
a124146
fix(theme): Light/Dark toggle actually switches (force NSApp.appearance)
Jul 28, 2026
fc6c640
fix(onboarding): stage slider — focus ring, end clipping, dark-mode t…
Jul 28, 2026
ece2ead
polish(chat): 3 minors — empty-row actions, header inset, thread title
Jul 28, 2026
2a24dd4
fix(chat): responsive column — shrink-to-fit with a consistent gutter
Jul 28, 2026
a906377
fix(chat): unify hero composer + cards to 760 (no width jump on first…
Jul 28, 2026
c1d2aab
feat(onboarding): cut the companion-picker step
Jul 28, 2026
466aa3a
feat(chat): persist chat threads to Firestore + hydrate on launch
Jul 28, 2026
1447f62
feat(chat): realistic mock content for front-end demo (no key needed)
Jul 28, 2026
890c4b5
docs: refresh HANDOFF for session close (chat workstream done, persis…
Jul 28, 2026
f13b15e
Chat: add breathing room to conversation spacing
Jul 28, 2026
6fad910
feat(chat): resume the last thread on launch when it is still recent
Jul 28, 2026
b08b7fe
feat(chat): DEBUG flag to shrink the resume window for testing
Jul 28, 2026
ee66f64
docs: spec for chat scenario mockups + parallel agents-working UI
Jul 28, 2026
c73e428
docs: implementation plan for chat scenario mockups + agents-working UI
Jul 28, 2026
e6af376
feat(chat): AgentRun model + math for parallel agents-working UI
Jul 28, 2026
2f457e1
feat(chat): AgentsWorkingRow — inline parallel agents view + preview
Jul 28, 2026
25b52fb
feat(chat): mock harness + "with the user" scenario preview
Jul 28, 2026
dcca807
feat(chat): roadmap / tasks / environment scenario previews
Jul 28, 2026
26949cd
feat(chat): in-app dev gallery for the chat mocks
Jul 28, 2026
feb7cf1
docs: spec for parallel department agents (live fan-out)
Jul 28, 2026
688d7e3
docs: implementation plan for parallel department agents (live fan-out)
Jul 28, 2026
142813d
feat(chat): RoadmapEngine.nextMoves planner for parallel fan-out
Jul 28, 2026
ca17c96
feat(chat): CompanyStore fan-out engine (parallel dept agents)
Jul 28, 2026
c08737d
feat(chat): wire the parallel fan-out into the chat UI
Jul 28, 2026
d102b88
polish(chat): dim composer during fan-out (isBusy includes isFanningOut)
Jul 28, 2026
a2f875d
fix(chat): clear fan-out flags on account switch + reset
Jul 28, 2026
f1d783d
test(chat): full-flow tests for the parallel agents fan-out
Jul 28, 2026
3e8ebd2
test(chat): self-contained mock company for offline fan-out testing
Jul 28, 2026
20c7223
feat(chat): typing "Run my next moves" triggers the fan-out too
Jul 28, 2026
38c209d
test(chat): mock a runnable task per department + keyword router
Jul 28, 2026
af4522a
test(chat): guided one-at-a-time mock flow (summary → focus → next)
Jul 28, 2026
d12f89f
fix(chat): strip ** bold markers from mock chat replies
Jul 28, 2026
454a4b9
fix(chat): chat-run + approve advance the roadmap (no duplicate drafts)
Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# HANDOFF — feat/chat-redesign (Task 1: chat)

_Last updated: Jul 28 2026, end of session. Tip `1447f62`._

## Status
- **Branch `feat/chat-redesign` (PR #39) — OPEN, HELD. Do NOT merge to `main` without the founder's explicit go.** GitHub reports CLEAN / MERGEABLE (main not diverged), 64 commits today.
- PR **#37** (chat-first shell) and **#40** (Firebase test-host flake fix) already **merged to main**.
- **Chat UI/UX workstream = 16/16 Done** on the W1 board (page `3aa3af4aaa9281b1bc09d051a59b0ca6`). "Done" = work-complete + founder-verified; still branch-held (Done-on-board ≠ merged).
- **AI is DOWN** — no Anthropic credits/key. Use the **mock** for any front-end display (below).

## What's built on this branch
Redesign (un-bubbled messages + thumbs, one card grammar, luminous orb, dept chips, live empty-state) · **streaming execute-log** · **dept→companion handoff** (specialists appear as pet sprites) · **thread header** (dropdown switcher + Share=copy transcript) · **dark mode** (Light/Dark toggle works via `NSApp.appearance`; `onAccent` text) · **responsive** column (760 + 24pt gutter, shrink-to-fit) · **chat persistence** (Firestore `companies/{uid}/threads/{id}`, survives restart) · **DEBUG mock harness**.

## Run it (mock, no key needed)
```
# build TEAM-SIGNED, signed build LAST (tests clobber signing), then verify:
xcodebuild build -project CodePet.xcodeproj -scheme codepet -destination 'platform=macOS' \
-allowProvisioningUpdates CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM=YL72VTKBR7 "CODE_SIGN_IDENTITY=Apple Development"
codesign -dv <app> 2>&1 | grep TeamIdentifier # MUST be YL72VTKBR7 (not adhoc — else Firebase keychain breaks)
# launch with the offline mock ON:
open -n <app> --args -CODEPET_MOCK_CHAT YES
```
Mock is keyword-routed: any question → advisor reply; `run my first task` → execute-log → tailored draft (positioning / landing copy / waitlist / pricing / interview); `remember…` → Noted chip; `connect…` → enable card; `roadmap`/dept chip → nav/handoff. Seed the board via **Roadmap** first if "no task to run".

## Gotchas
- `xcodebuild test … CODE_SIGNING_ALLOWED=NO` rebuilds the app **ad-hoc** into the same DerivedData → breaks Firebase keychain on launch. Always: tests first, **signed build last**, verify TeamIdentifier, then launch.
- Canonical work checkout drifted to `~/Developer/codepet` (iCloud evicted ~/Documents & ~/Desktop repos; `~/codepet-chat` is behind origin). Prefer a fresh non-iCloud checkout of `feat/chat-redesign`.
- **Two parallel sessions** run — own branch, own worktree, serialized app testing (one launches at a time). See memory `codepet-parallel-session-protocol`.

## Next candidates (not started)
Message-cap per thread · search Recent · clear-all-history. Add a W1 board row before building (the board is the source of truth).

## Resume-last-thread on launch (built, unverified in-app)
Launch now reopens the newest thread when it was touched inside `threadResumeWindow` (**8h**), else opens the live hero — founder's call, so a new day still starts on the time-of-day greeting + roadmap cards. Pure `pickResumeThreadId(in:now:within:)` in `ChatThreads.swift`, called from `CompanyStore.hydrate`. Two adjacent fixes came with it: the transcript now pins to its newest message when it arrives already-populated (resume, or switching into an existing thread — it used to open at the OLDEST message, since it only scrolled on a message-COUNT change), and the enrichment interview survives a relaunch (`interviewState` is session-only, so answering a resumed question used to save the answer and then go silent; `EnrichInterview.remainingGaps` rebuilds the cursor from the brief minus what the transcript already answered, so a skip stays skipped). 10 new tests. **Not yet exercised in the running app** — needs a signed build + a real quit/relaunch inside the window.

DEBUG-only `-CODEPET_RESUME_WINDOW_SECONDS <n>` (same idiom as `-CODEPET_MOCK_CHAT`, only a POSITIVE value counts) shrinks the window so the stale branch is testable in seconds instead of eight hours. Driver script: `~/Developer/codepet-test-resume.sh` — `launch` / `relaunch` (the quit→resume loop, graceful quit so pending thread writes aren't cut off) / `stale` / `lock`.

**Test-suite gotcha found this session:** any running `codepet.app` holds `~/Library/Application Support/firestore/__FIRAPP_DEFAULT/devpet-8f4b1/main/LOCK`, and a second process that initializes Firestore aborts in `FirestoreClient::Initialize` (SIGABRT). That is the "Firebase test-host flake" — every test class that builds a `CompanyStore` with the DEFAULT `threadsLoader` dies, 0 tests executed, while classes injecting fakes pass. Close the app before `xcodebuild test`.
35 changes: 33 additions & 2 deletions codepet/App/CodePetApp.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import AppKit
import FirebaseCore
import GoogleSignIn

Expand Down Expand Up @@ -42,9 +43,28 @@ struct CodePetApp: App {
_chatController = StateObject(wrappedValue: chatController)
}

/// Force the app's effective appearance so the CodepetTheme dyn NSColor tokens
/// re-resolve on a Light/Dark pick. `.system` → nil (follow macOS).
private static func applyAppearance(_ theme: AppTheme) {
switch theme {
case .system: NSApplication.shared.appearance = nil
case .light: NSApplication.shared.appearance = NSAppearance(named: .aqua)
case .dark: NSApplication.shared.appearance = NSAppearance(named: .darkAqua)
}
}

var body: some Scene {
WindowGroup {
ContentView()
Group {
#if DEBUG
// Dev-only: `-CODEPET_MOCK_GALLERY YES` swaps the root for the
// chat-mocks gallery so the scenario mockups are viewable in a
// running build (see ChatMocksGalleryView).
if MockGallery.enabled { ChatMocksGalleryView() } else { ContentView() }
#else
ContentView()
#endif
}
.environment(\.font, CodepetTheme.body(13))
.environment(\.uiLanguage, appState.uiLanguage)
.environmentObject(appState)
Expand Down Expand Up @@ -73,9 +93,20 @@ struct CodePetApp: App {
.environmentObject(learnProgress)
.environmentObject(challengeProgress)
.environmentObject(feedbackManager)
.frame(minWidth: 400, minHeight: 700)
// 560 = the onboarding card's real floor: a 320pt clamped art panel
// (OnboardingLayout.artWidth) + the form's 64pt horizontal padding either
// side + room for the 600pt measure to compress into. At 400 the form
// panel was driven to a negative width and the layout clipped.
.frame(minWidth: 560, minHeight: 700)
.themed(isDark: appState.appTheme == .dark)
.preferredColorScheme(appState.appTheme.colorScheme)
// preferredColorScheme drives SwiftUI-native colors, but the
// CodepetTheme.dyn tokens resolve via dynamic NSColor keyed on the
// app's EFFECTIVE appearance — which preferredColorScheme does NOT
// force. So a Light/Dark pick would leave the tokens on the system
// appearance. Force NSApp.appearance to make every dyn token flip.
.onChange(of: appState.appTheme) { _, theme in Self.applyAppearance(theme) }
.onAppear { Self.applyAppearance(appState.appTheme) }
.task {
projectStore.load()
TipsPersistence.shared.load(into: tipsState)
Expand Down
51 changes: 44 additions & 7 deletions codepet/Managers/ChatThreads.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
// codepet/Managers/ChatThreads.swift
import Foundation

/// One session-only chat conversation — a named bucket of `CopilotMessage`s.
/// Deliberately NOT Codable: Level 1 multi-thread history is in-memory only,
/// same as `chatMessages` itself (see `CopilotMessage`'s doc comment). Native
/// port of the web `ThreadMeta` (`lib/firebase/schema.ts`) + its message array,
/// merged into one struct since there's no persistence layer to split them.
struct ChatThread: Identifiable, Equatable {
/// One chat conversation — a named bucket of `CopilotMessage`s. Codable so a
/// thread can be persisted to `companies/{uid}/threads/{id}` and hydrated on
/// launch. Native port of the web `ThreadMeta` (`lib/firebase/schema.ts`) + its
/// message array, merged into one struct.
struct ChatThread: Identifiable, Equatable, Codable {
let id: String
/// nil until the founder's first message derives one (or a rename sets a
/// non-blank title) — rendered as "New chat" by the view, mirrors the web.
var title: String?
var messages: [CopilotMessage]
let createdAt: Date
var updatedAt: Date

/// A copy safe to PERSIST: drops transient "producing" placeholders (they
/// exist only mid-run) and clears streaming-only `execSteps`, so a saved
/// thread never resurrects a half-finished run on the next launch.
var persistable: ChatThread {
var copy = self
copy.messages = messages
.filter { !$0.producing }
.map { m in var mm = m; mm.execSteps = nil; return mm }
return copy
}
}

// Pure, unit-testable helpers behind the thread switcher — ported from the
Expand All @@ -30,7 +40,12 @@ private let threadTitleMax = 40
/// ellipsis. Returns nil when there is no `.me` message yet, OR its text is
/// blank — the view falls back to "New chat" for either case, same as the web.
func deriveThreadTitle(_ messages: [CopilotMessage]) -> String? {
guard let first = messages.first(where: { $0.role == .me }) else { return nil }
// Prefer the founder's first message; fall back to the first non-empty
// message of ANY role so a thread that so far only holds byte's seeded
// question/greeting still gets a distinguishable name instead of "New chat".
let source = messages.first(where: { $0.role == .me && !$0.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
?? messages.first(where: { !$0.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
guard let first = source else { return nil }
let collapsed = first.text.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !collapsed.isEmpty else { return nil }
Expand All @@ -52,6 +67,28 @@ func pickFallbackThreadId(after deletedId: String, in threads: [ChatThread]) ->
sortThreadsByRecent(threads.filter { $0.id != deletedId }).first?.id
}

/// How long after its last message a thread still counts as the founder's
/// current working stretch. Inside it, launch resumes that thread; outside it,
/// launch opens the live hero instead — so quitting and reopening keeps your
/// place, while a new day still starts on the time-of-day greeting and the
/// roadmap landing cards. One constant to tune.
let threadResumeWindow: TimeInterval = 8 * 3600

/// Which thread launch should reopen, or nil to land on the empty hero. The
/// newest thread that actually holds messages, provided it was touched within
/// `window`. `now` is a parameter (not `Date()` inside) for testability, same as
/// `relativeTime`. A future-dated `updatedAt` (clock skew from another device)
/// counts as recent rather than stale.
///
/// Order-agnostic: callers may pass threads in any order (a Firestore query's
/// ordering isn't guaranteed to survive a decode-and-filter pass), so this sorts
/// before picking rather than trusting the input.
func pickResumeThreadId(in threads: [ChatThread], now: Date, within window: TimeInterval = threadResumeWindow) -> String? {
guard let newest = sortThreadsByRecent(threads).first(where: { !$0.messages.isEmpty }) else { return nil }
guard now.timeIntervalSince(newest.updatedAt) <= window else { return nil }
return newest.id
}

/// Compact relative time for the history list — "just now" under a minute,
/// then minutes/hours/days. English only (the pure helper stays language-
/// neutral like the rest of `ChatThreads`; the view localizes surrounding
Expand Down
Loading