Skip to content

Commit bd35e72

Browse files
committed
Show a first-launch primer on the app's vocabulary
One scrollable card — loop, node, edge, the four loop types, Quick Chat, project — shown once on first launch and reopenable from the sidebar's help button. A new user's first sight of the app is an empty canvas whose words mean nothing yet; five short definitions is the cheapest fix there is.
1 parent 85f3922 commit bd35e72

3 files changed

Lines changed: 111 additions & 0 deletions

File tree

graphcode/Sources/Features/App/AppFeature.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ struct AppFeature {
3333
/// Whether the open workspace is a quick chat rather than a graph node's loop.
3434
func isQuickChat(_ nodeID: UUID) -> Bool { quickChats[id: nodeID] != nil }
3535

36+
/// Up on first launch (`.task` checks the persisted flag) and whenever the
37+
/// sidebar's help button asks for it again.
38+
var showingOnboarding = false
39+
3640
/// The orchestrator's needs-attention rollup, across every open project
3741
/// (docs/05-orchestrator.md#monitoring-surface). Derived rather than stored: it's a
3842
/// pure function of the graphs the daemon already broadcasts, and a cached copy
@@ -110,6 +114,9 @@ struct AppFeature {
110114
case selectPreviousLoop
111115
/// The stop/kill affordance docs/05-orchestrator.md asks the monitor for.
112116
case stopNodeTapped(projectPath: String, nodeID: UUID)
117+
/// The first-launch terminology primer — see `OnboardingView`.
118+
case onboardingRequested
119+
case onboardingDismissed
113120
/// The Quick Chats section's actions — see `State.quickChats`.
114121
case newQuickChatTapped
115122
case quickChatTapped(UUID)
@@ -136,6 +143,11 @@ struct AppFeature {
136143
switch action {
137144
case .task:
138145
state.quickChats = IdentifiedArray(uniqueElements: quickChatStore.load())
146+
// Once, not every launch: the primer's value is on day one, and re-showing it
147+
// to someone who has loops running would read as the app forgetting them.
148+
if !UserDefaults.standard.bool(forKey: "hasSeenOnboarding") {
149+
state.showingOnboarding = true
150+
}
139151
return .merge(
140152
.run { send in
141153
for await event in orchestratorClient.connect() {
@@ -290,6 +302,15 @@ struct AppFeature {
290302
.graphCommand(projectPath: projectPath, command: .stopNode(nodeID)))
291303
}
292304

305+
case .onboardingRequested:
306+
state.showingOnboarding = true
307+
return .none
308+
309+
case .onboardingDismissed:
310+
state.showingOnboarding = false
311+
UserDefaults.standard.set(true, forKey: "hasSeenOnboarding")
312+
return .none
313+
293314
case .newQuickChatTapped:
294315
let chat = QuickChat(
295316
title: "Chat — \(Date().formatted(.dateTime.month(.abbreviated).day()))",

graphcode/Sources/Features/App/AppView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ struct AppView: View {
2121
}
2222
.frame(maxWidth: .infinity, maxHeight: .infinity)
2323
.background(Theme.windowBackground)
24+
// First launch only (and the sidebar's help button after that): the app's
25+
// vocabulary in one card, before the empty canvas has to explain itself.
26+
.sheet(
27+
isPresented: Binding(
28+
get: { store.showingOnboarding },
29+
set: { if !$0 { store.send(.onboardingDismissed) } }
30+
)
31+
) {
32+
OnboardingView { store.send(.onboardingDismissed) }
33+
}
2434
// Paints the window itself, so the titlebar and toolbar match instead of sitting a
2535
// shade lighter above the content.
2636
.containerBackground(Theme.windowBackground, for: .window)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import SwiftUI
2+
3+
/// A first-launch primer on the app's vocabulary — loop, node, edge, and the four loop
4+
/// types — shown once (see `AppFeature.State.showingOnboarding`) and reopenable from
5+
/// the sidebar's help button. One scrollable card rather than a paged carousel: there
6+
/// are six ideas to convey and a reader should be able to see them all, not click
7+
/// through them.
8+
struct OnboardingView: View {
9+
let onDismiss: () -> Void
10+
11+
var body: some View {
12+
VStack(spacing: 0) {
13+
VStack(spacing: 6) {
14+
Image(systemName: "point.3.connected.trianglepath.dotted")
15+
.font(.system(size: 34))
16+
.foregroundStyle(Color.accentColor)
17+
Text("Welcome to GraphCode").font(.title2).bold()
18+
Text("Graphs of live, steerable AI coding sessions. Five words to know:")
19+
.font(.subheadline)
20+
.foregroundStyle(.secondary)
21+
}
22+
.padding(.top, 28)
23+
.padding(.bottom, 16)
24+
25+
ScrollView {
26+
VStack(alignment: .leading, spacing: 14) {
27+
term(
28+
"circle.circle", "Loop",
29+
"A unit of work an AI coding agent runs in a real terminal session — one you "
30+
+ "can open, watch, and steer mid-run. Loops keep running with the app closed.")
31+
term(
32+
"point.3.connected.trianglepath.dotted", "Node",
33+
"A loop as drawn on the graph canvas. Click a node to attach to its live "
34+
+ "terminal — scrollback and all.")
35+
term(
36+
"arrow.right", "Edge",
37+
"A connection between loops, drawn by dragging between nodes: a hand-off "
38+
+ "that fires when the source finishes, a message, or a spawn.")
39+
term(
40+
"flag.checkered", "Loop types",
41+
"Goal-based runs until a goal is met. Time-based repeats on a cadence in its "
42+
+ "own prompt — watchers live here. Turn-based pauses for your review each "
43+
+ "turn. Proactive is a group of loops run as one.")
44+
term(
45+
"bubble.left", "Quick Chat",
46+
"Just a conversation — no goal, no graph. The sidebar section above your "
47+
+ "projects.")
48+
term(
49+
"folder", "Project",
50+
"A folder you add. It gets its own graph of loops; nothing is ever written "
51+
+ "inside the folder itself.")
52+
}
53+
.padding(.horizontal, 28)
54+
}
55+
56+
Button("Get Started") { onDismiss() }
57+
.keyboardShortcut(.defaultAction)
58+
.controlSize(.large)
59+
.padding(.vertical, 20)
60+
}
61+
.frame(width: 480, height: 560)
62+
}
63+
64+
private func term(_ glyph: String, _ name: String, _ explanation: String) -> some View {
65+
HStack(alignment: .top, spacing: 12) {
66+
Image(systemName: glyph)
67+
.font(.body)
68+
.foregroundStyle(Color.accentColor)
69+
.frame(width: 22)
70+
VStack(alignment: .leading, spacing: 2) {
71+
Text(name).font(.headline)
72+
Text(explanation).font(.callout).foregroundStyle(.secondary)
73+
}
74+
}
75+
}
76+
}
77+
78+
#Preview {
79+
OnboardingView(onDismiss: {})
80+
}

0 commit comments

Comments
 (0)