Skip to content

Commit 9dc672c

Browse files
authored
docs: showcase Pine 2.0 release (#1332)
* docs: showcase Pine 2.0 release * fix: stabilize agent inbox screenshot test
1 parent ce64fe1 commit 9dc672c

9 files changed

Lines changed: 463 additions & 13 deletions

File tree

Pine/Agent/AgentTaskRegistry.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,157 @@ final class AgentTaskRegistry {
172172
self.flushTail = flushTail
173173
}
174174

175+
#if DEBUG
176+
/// Seeds stable, human-readable rows for the opt-in marketing screenshot.
177+
/// The fixture is presentation-only: it is never persisted or indexed as
178+
/// live terminal ownership, and production builds contain no entry point.
179+
func seedMarketingInboxForUITesting(at referenceDate: Date = Date()) {
180+
guard tasks.isEmpty else { return }
181+
tasks = [
182+
marketingTask(.init(
183+
seed: 1,
184+
projectName: "Pine Demo",
185+
worktreeName: nil,
186+
title: "Approve the release announcement",
187+
agentType: .codex,
188+
state: .waitingInput,
189+
liveness: .live,
190+
lifecycle: .active,
191+
attention: .waitingInput,
192+
isUnread: true,
193+
startedSecondsAgo: 7 * 60,
194+
verifiedSecondsAgo: 18
195+
), referenceDate: referenceDate),
196+
marketingTask(.init(
197+
seed: 2,
198+
projectName: "Launch Site",
199+
worktreeName: nil,
200+
title: "Verify signed DMG and Homebrew install",
201+
agentType: .claudeCode,
202+
state: .done,
203+
liveness: .terminated,
204+
lifecycle: .completed,
205+
attention: .completed,
206+
isUnread: true,
207+
startedSecondsAgo: 18 * 60,
208+
verifiedSecondsAgo: 2 * 60
209+
), referenceDate: referenceDate),
210+
marketingTask(.init(
211+
seed: 3,
212+
projectName: "Pine Demo",
213+
worktreeName: "release-2.0",
214+
title: "Capture Agent Inbox screenshots",
215+
agentType: .gemini,
216+
state: .executing,
217+
liveness: .live,
218+
lifecycle: .active,
219+
attention: .none,
220+
isUnread: false,
221+
startedSecondsAgo: 4 * 60,
222+
verifiedSecondsAgo: 8
223+
), referenceDate: referenceDate),
224+
marketingTask(.init(
225+
seed: 4,
226+
projectName: "Documentation",
227+
worktreeName: nil,
228+
title: "Translate the Pine 2.0 highlights",
229+
agentType: .openCode,
230+
state: .thinking,
231+
liveness: .live,
232+
lifecycle: .active,
233+
attention: .none,
234+
isUnread: false,
235+
startedSecondsAgo: 2 * 60,
236+
verifiedSecondsAgo: 5
237+
), referenceDate: referenceDate),
238+
]
239+
}
240+
241+
private struct MarketingTaskFixture {
242+
let seed: Int
243+
let projectName: String
244+
let worktreeName: String?
245+
let title: String
246+
let agentType: AgentType
247+
let state: AgentRunState
248+
let liveness: AgentRunLiveness
249+
let lifecycle: AgentTaskLifecycle
250+
let attention: AgentTaskAttention
251+
let isUnread: Bool
252+
let startedSecondsAgo: TimeInterval
253+
let verifiedSecondsAgo: TimeInterval
254+
}
255+
256+
private func marketingTask(
257+
_ fixture: MarketingTaskFixture,
258+
referenceDate: Date
259+
) -> AgentTask {
260+
let projectPath = "/Pine Marketing/\(fixture.projectName)"
261+
let worktreePath = fixture.worktreeName.map {
262+
"\(projectPath)/.pine-worktrees/\($0)"
263+
} ?? projectPath
264+
let terminalID = marketingUUID(fixture.seed + 1_000)
265+
let observedAt = referenceDate.addingTimeInterval(
266+
-fixture.verifiedSecondsAgo
267+
)
268+
let startedAt = referenceDate.addingTimeInterval(
269+
-fixture.startedSecondsAgo
270+
)
271+
let context = AgentTaskBridgeContext(
272+
project: AgentTaskProjectIdentity(
273+
canonicalProjectPath: projectPath,
274+
canonicalWorktreePath: worktreePath
275+
),
276+
route: AgentTaskRoute(
277+
paneID: marketingUUID(fixture.seed),
278+
tabID: terminalID,
279+
terminalID: terminalID,
280+
availability: fixture.liveness == .live
281+
? .available
282+
: .missing
283+
),
284+
origin: .pineLaunched,
285+
observedAt: startedAt
286+
)
287+
var task = AgentTask(
288+
descriptor: AgentDescriptor(agentType: fixture.agentType),
289+
context: context,
290+
title: fixture.title,
291+
objective: fixture.title,
292+
createdAt: startedAt
293+
)
294+
task.runs = [AgentTaskRun(AgentTaskRunInput(
295+
id: marketingUUID(fixture.seed + 2_000),
296+
terminalID: terminalID,
297+
process: AgentProcessEvidence(
298+
processIdentifier: Int32(20_000 + fixture.seed),
299+
processGeneration: UInt64(fixture.seed),
300+
startIdentifier: "marketing-fixture-\(fixture.seed)",
301+
observedStartedAt: startedAt,
302+
startIsAuthoritative: true
303+
),
304+
status: AgentTaskRunStatus(
305+
state: fixture.state,
306+
liveness: fixture.liveness,
307+
observedAt: observedAt
308+
)
309+
))]
310+
task.lifecycle = fixture.lifecycle
311+
task.attention = fixture.attention
312+
task.isUnread = fixture.isUnread
313+
task.updatedAt = observedAt
314+
task.lastActivityAt = observedAt
315+
task.completedAt = fixture.lifecycle == .completed ? observedAt : nil
316+
return task
317+
}
318+
319+
private func marketingUUID(_ seed: Int) -> UUID {
320+
let suffix = String(format: "%012llX", UInt64(seed))
321+
return UUID(uuidString: "00000000-0000-0000-0000-\(suffix)")
322+
?? UUID()
323+
}
324+
#endif
325+
175326
func task(for id: UUID) -> AgentTask? {
176327
tasks.first { $0.id == id }
177328
}

Pine/ProjectRegistry.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ final class ProjectRegistry: LSPSettingsObserver {
8181
defaults.removeObject(forKey: Self.recentProjectsKey)
8282
}
8383
loadRecentProjects()
84+
#if DEBUG
85+
if ProcessInfo.processInfo.arguments.contains(
86+
"--ui-test-agent-inbox-marketing"
87+
) {
88+
agentTasks.seedMarketingInboxForUITesting()
89+
}
90+
#endif
8491
lspSettings.addObserver(self)
8592
}
8693

Pine/WelcomeView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ struct WelcomeView: View {
100100
Button {
101101
openWindow(id: "agent-inbox")
102102
NSApp.activate()
103+
#if DEBUG
104+
if ProcessInfo.processInfo.arguments.contains(
105+
"--ui-test-agent-inbox-marketing"
106+
) {
107+
appDelegate?.hideWelcome()
108+
}
109+
#endif
103110
} label: {
104111
Label(Strings.menuAgentInbox, systemImage: MenuIcons.agentInbox)
105112
}

PineUITests/ScreenshotTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,57 @@ final class ScreenshotTests: PineUITestCase {
4848
attachScreenshot(screenshot, name: "screenshot-welcome")
4949
}
5050

51+
// MARK: - Agent Inbox
52+
53+
func testCaptureAgentInbox() throws {
54+
app.launchArguments += [
55+
"--clear-recent-projects",
56+
"--ui-test-agent-inbox-marketing",
57+
]
58+
launchClean()
59+
60+
let welcomeWindow = app.windows["welcome"]
61+
XCTAssertTrue(
62+
waitForExistence(welcomeWindow, timeout: 10),
63+
"Welcome window should appear"
64+
)
65+
66+
let inboxButton = app.buttons["welcomeAgentInboxButton"]
67+
XCTAssertTrue(
68+
waitForExistence(inboxButton, timeout: 5),
69+
"Agent Inbox should be available from Welcome"
70+
)
71+
inboxButton.click()
72+
73+
let inboxWindow = app.windows["Agent Inbox"]
74+
XCTAssertTrue(
75+
waitForExistence(inboxWindow, timeout: 10),
76+
"Agent Inbox window should appear"
77+
)
78+
// The marketing launch path hides every SwiftUI/AppKit Welcome owner.
79+
// This avoids closing only one of the duplicate fallback windows that
80+
// macOS 26 can expose to XCUITest.
81+
XCTAssertTrue(
82+
welcomeWindow.waitForNonExistence(timeout: 5),
83+
"Welcome should hide before capturing Agent Inbox"
84+
)
85+
let releaseTask = inboxWindow.buttons.matching(
86+
NSPredicate(
87+
format: "label CONTAINS %@",
88+
"Approve the release announcement"
89+
)
90+
).firstMatch
91+
XCTAssertTrue(
92+
waitForExistence(releaseTask, timeout: 5),
93+
"Marketing tasks should populate Agent Inbox"
94+
)
95+
96+
Thread.sleep(forTimeInterval: 1.0)
97+
98+
let screenshot = inboxWindow.screenshot()
99+
attachScreenshot(screenshot, name: "screenshot-agent-inbox")
100+
}
101+
51102
// MARK: - Editor with File
52103

53104
func testCaptureEditorWithFile() throws {

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
# Pine
1+
<p align="center">
2+
<img src="assets/pine-app-icon.png" width="144" alt="Pine app icon">
3+
</p>
4+
5+
<h1 align="center">Pine</h1>
26

37
[![CI](https://github.com/batonogov/pine/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/batonogov/pine/actions/workflows/ci.yml)
48
[![Release](https://img.shields.io/github/v/release/batonogov/pine)](https://github.com/batonogov/pine/releases/latest)
@@ -14,6 +18,19 @@
1418

1519
Pine keeps CLI agents in the terminal and the code in view. It reflects agent activity, file changes, diagnostics, and Git context across a fast native workspace without turning into another AI dashboard. Built with SwiftUI and AppKit for macOS 26 Liquid Glass — no Electron runtime.
1620

21+
## New in Pine 2.0
22+
23+
<p align="center">
24+
<img src="assets/screenshot-agent-inbox.png" width="760" alt="Pine Agent Inbox showing agent tasks across projects">
25+
</p>
26+
27+
- **Cross-project Agent Inbox** keeps durable tasks together and routes back to the exact live session
28+
- **Safer agent workflows** add isolated worktrees, task recovery, and evidence-based completion briefs
29+
- **Useful signals without surveillance** provide actionable, privacy-bounded notifications and first-party CLI-agent adapters
30+
- **A polished release experience** brings a branded DMG installer and Pine's new app icon
31+
32+
See the [Pine 2.0 release notes](https://github.com/batonogov/pine/releases/tag/v2.0.0) for the complete changelog.
33+
1734
## Features
1835

1936
- **Native macOS** — SwiftUI + AppKit, Liquid Glass UI, system text handling. No browser engine, no runtime

assets/pine-app-icon.png

391 KB
Loading

assets/screenshot-agent-inbox.png

169 KB
Loading

0 commit comments

Comments
 (0)