Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
20cf4af
Ship tiling pane redesign: all panes optimized for 400-600px tiles
max4c Apr 3, 2026
0537f2c
Terminal: fix paste, load Ghostty config, prevent parent TTY wipe
max4c Apr 3, 2026
270121c
Meeting block: reduce title-to-notes gap from 16pt to 4pt
max4c Apr 3, 2026
9af3d62
Callout block: remove left border, match TOC container styling
max4c Apr 3, 2026
4ab7eff
Outline block: grey text links instead of bullet circles
max4c Apr 3, 2026
3763433
Table block: remove duplicate grip dots, clean grid layout
max4c Apr 3, 2026
e80d5e5
Select option kebab: larger hit area, popover beside not on top
max4c Apr 3, 2026
7c88eb4
Chat redesign: rename Ask AI → Chat, remove X, Cmd+I opens sidebar
max4c Apr 3, 2026
d848bbe
Mention links: render @[[Page Name]] as styled inline link
max4c Apr 3, 2026
e60f711
Cmd+K navigation: use DispatchQueue.main.async to escape SwiftUI tran…
max4c Apr 3, 2026
525abcf
Mail: auto-refresh inbox every 60 seconds while pane is visible
max4c Apr 3, 2026
4ef4dbb
Table grouped view: collapsed groups reclaim vertical space
max4c Apr 3, 2026
8d5996a
Heading toggles: Cmd+Shift+Enter toggle + auto-nest smaller headings
max4c Apr 3, 2026
96bdf89
Merge branch 'worktree-agent-aa794b17' into dev
max4c Apr 3, 2026
b197355
Update go run progress: 15 tickets completed
max4c Apr 3, 2026
617dd7e
Review feedback: 7 quick fixes from smoke test + regenerate xcodeproj
max4c Apr 3, 2026
a9e5e6c
Review round 2: TOC markdown strip, Cmd+I sidebar, tab icons, grouped…
max4c Apr 3, 2026
2d27205
Review round 3: sidebar Chat opens full window, TOC skips H1, chat ta…
max4c Apr 3, 2026
a8bd5c3
Review round 4: chat gets tab bar, TOC uses tree depth not heading le…
max4c Apr 3, 2026
fa407b1
TOC hover highlight, fix grouped table blank space for real
max4c Apr 3, 2026
aec5f56
Grouped table: wrap entire DB in vertical scroll, fix TOC row coloring
max4c Apr 3, 2026
0fd0705
Set window title to 'Bugbook' for Mission Control identification
max4c Apr 3, 2026
9a1e55b
Grouped table: add 48pt bottom padding inside scroll content
max4c Apr 3, 2026
efc160f
TOC: respect block background color on container
max4c Apr 3, 2026
939e3d4
Database grouped table: pin header, table fills remaining space (Codex)
max4c Apr 3, 2026
c1039af
Review fixes: mail webview reload guard, clipboard thread safety
max4c Apr 3, 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
52 changes: 47 additions & 5 deletions .go/progress.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# Go Run — 2026-03-31
Started: 05:18 PM
Time budget: 8h
Queue: 12 tickets from .go/queue.json
Worker mix: 9 Codex, 3 Claude agent
# Go Run — 2026-04-03

Started: 12:39 AM
Finished: 1:18 AM
Duration: 39m
Time utilization: 39m/8h (8%)

## Completed (15 tickets, all verified via build)
- [x] Terminal paste fix (row_8h9iqr) — direct — PASS
- [x] Terminal history wipe fix (row_23xqr5) — direct — PASS
- [x] Ghostty config import — direct — PASS
- [x] Meetings rename (row_wcsgow) — direct — PASS (already correct)
- [x] Meeting block padding (row_k1pfpn) — direct — PASS
- [x] Callout block fix (row_fnmxx9) — direct — PASS
- [x] Outline/TOC fix (row_u9mndd) — direct — PASS
- [x] Heading toggles (row_b7h2vl) — Claude agent — PASS (2/3 behaviors)
- [x] TableBlockView fix (row_srmgse) — direct — PASS
- [x] Kebab menu fix (row_0lsztg) — direct — PASS
- [x] Chat redesign (row_qm7iyh) — direct — PASS
- [x] Mention picker styling (row_dimm5g) — direct — PASS
- [x] Cmd+K navigation fix (row_uqw8vz) — direct — PASS
- [x] Mail auto-refresh (row_iibyiq) — direct — PASS
- [x] Table grouped collapse (row_6pk1v8) — direct — PASS

## Review Queue (15 tickets)
All moved to Review status in Bugbook.

## Partial
- Heading toggles: Cmd+Shift+Enter and auto-nesting work. Enter-exits-toggle not yet implemented.

## Skipped (valid reasons)
- Google OAuth verification (row_rv254w) — external blocker, needs domain + Google Console
- Canopy tickets (4) — different repo
- Mobile capture UX (row_vk26pw) — research note, no implementation
- Live knowledge retrieval (row_25nsk1) — research/future
- Lookup/Rollup/Formula fields — medium priority, larger scope

## Discoveries
- ghostty_init(0, nil) prevents parent terminal TTY manipulation
- ghostty_config_load_default_files loads ~/.config/ghostty/config
- Cmd+K nav failed 3 times due to SwiftUI transaction swallowing @State changes; DispatchQueue.main.async fixes it
- TableBlockView had duplicate grip dots from both BlockCellView and its own gripDotsColumn
- Multiple Bugbook processes (release, Xcode debug, swift build) can interfere

## How to Review
git checkout dev
swift build && .build/arm64-apple-macosx/debug/Bugbook
1 change: 1 addition & 0 deletions .go/start_time
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1775201074
1 change: 1 addition & 0 deletions Sources/Bugbook/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct MCPServerInfo: Identifiable {
var isRecording: Bool = false
var recordingBlockId: UUID?
var flashcardReviewOpen: Bool = false
var showShortcutOverlay: Bool = false
@ObservationIgnored lazy var aiThreadStore = AiThreadStore()

var activeTab: OpenFile? {
Expand Down
13 changes: 12 additions & 1 deletion Sources/Bugbook/App/BugbookApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ struct BugbookApp: App {
}
.keyboardShortcut("p")

Button("Ask AI") {
Button("Chat") {
NotificationCenter.default.post(name: .openAIPanel, object: nil)
}
.keyboardShortcut("i")
Expand Down Expand Up @@ -218,6 +218,13 @@ struct BugbookApp: App {
}
.keyboardShortcut(",")
}

CommandGroup(replacing: .help) {
Button("Keyboard Shortcuts") {
NotificationCenter.default.post(name: .toggleShortcutOverlay, object: nil)
}
.keyboardShortcut("/")
}
}
}
}
Expand Down Expand Up @@ -363,6 +370,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
guard !window.titlebarAppearsTransparent else { continue }
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.title = "Bugbook"
window.styleMask.insert(.fullSizeContentView)
window.isMovableByWindowBackground = true
}
Expand All @@ -381,6 +389,7 @@ extension Notification.Name {
static let quickOpenNewTab = Notification.Name("quickOpenNewTab")
static let openSettings = Notification.Name("openSettings")
static let openAIPanel = Notification.Name("openAIPanel")
static let openFullChat = Notification.Name("openFullChat")
static let askAI = Notification.Name("askAI")
static let toggleTheme = Notification.Name("toggleTheme")
static let newDatabase = Notification.Name("newDatabase")
Expand All @@ -397,6 +406,8 @@ extension Notification.Name {
static let openCalendar = Notification.Name("openCalendar")
static let openMeetings = Notification.Name("openMeetings")
static let openGateway = Notification.Name("openGateway")
static let openTerminal = Notification.Name("openTerminal")
static let toggleShortcutOverlay = Notification.Name("toggleShortcutOverlay")
static let fileDeleted = Notification.Name("fileDeleted")
static let fileMoved = Notification.Name("fileMoved")
static let movePage = Notification.Name("movePage")
Expand Down
25 changes: 25 additions & 0 deletions Sources/Bugbook/Lib/AttributedStringConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ enum AttributedStringConverter {
continue
}

// Mention: @[[Page Name]] → styled inline link
if let (name, end) = parseMention(markdown, from: i) {
var attrs = baseAttributes
attrs[.foregroundColor] = NSColor.controlAccentColor
attrs[.underlineStyle] = NSUnderlineStyle.single.rawValue
attrs[Self.markdownSourceKey] = "@[[\(name)]]"
result.append(NSAttributedString(string: name, attributes: attrs))
i = end
continue
}

// Double-equals separator: " == " → arrow indicator
if let end = parseDoubleEqualsSeparator(markdown, from: i) {
var attrs = baseAttributes
Expand Down Expand Up @@ -234,6 +245,20 @@ enum AttributedStringConverter {
return nil
}

/// Parse mention: @[[Page Name]] → (name, endIndex)
private static func parseMention(
_ str: String,
from start: String.Index
) -> (String, String.Index)? {
let prefix = "@[["
guard str[start...].hasPrefix(prefix) else { return nil }
let nameStart = str.index(start, offsetBy: prefix.count)
guard let closingRange = str[nameStart...].range(of: "]]") else { return nil }
let name = String(str[nameStart..<closingRange.lowerBound])
guard !name.isEmpty else { return nil }
return (name, closingRange.upperBound)
}

/// Parse double-equals separator: " == " (with spaces on both sides)
private static func parseDoubleEqualsSeparator(
_ str: String,
Expand Down
38 changes: 37 additions & 1 deletion Sources/Bugbook/Models/BlockDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,39 @@ class BlockDocument {
}
}

/// Toggle expand/collapse for the enclosing toggle or headingToggle block.
/// Works whether the focused block is the toggle itself or a child inside it.
func toggleBlockExpanded(id: UUID) {
guard let loc = blockLocation(for: id) else { return }
let parentIdx = loc.topLevel
let parentType = blocks[parentIdx].type
guard parentType == .toggle || parentType == .headingToggle else { return }
withAnimation(.easeInOut(duration: 0.12)) {
blocks[parentIdx].isExpanded.toggle()
}
}

/// When converting a block to headingToggle, absorb subsequent sibling blocks
/// until hitting a heading/headingToggle of equal or larger size (level number <= ours).
/// Non-heading blocks are always absorbed.
func autoNestIntoHeadingToggle(blockId: UUID, level: Int) {
guard let loc = blockLocation(for: blockId), loc.child == nil else { return }
let idx = loc.topLevel
var absorbed: [Block] = []
while idx + 1 < blocks.count {
let sibling = blocks[idx + 1]
let isHeading = sibling.type == .heading || sibling.type == .headingToggle
if isHeading, sibling.headingLevel > 0, sibling.headingLevel <= level {
break
}
absorbed.append(blocks.remove(at: idx + 1))
}
if !absorbed.isEmpty {
blocks[idx].children.append(contentsOf: absorbed)
blocks[idx].isExpanded = true
}
}

// MARK: - Table Mutations

func updateTableCell(id: UUID, row: Int, col: Int, text: String) {
Expand Down Expand Up @@ -911,7 +944,7 @@ class BlockDocument {

static let slashCommands: [SlashCommand] = [
// Suggested
SlashCommand(name: "Ask AI", icon: "ladybug", action: .askAI, section: "Suggested", keywords: ["ai", "chat", "generate", "write"]),
SlashCommand(name: "Ask AI", icon: "ladybug", action: .askAI, section: "Suggested", keywords: ["ai", "chat", "generate", "write", "ask"]),
SlashCommand(name: "Image", icon: "photo", action: .imagePicker, section: "Suggested", keywords: ["photo", "picture", "media", "upload"]),
SlashCommand(name: "Template", icon: "doc.on.doc", action: .template, section: "Suggested", keywords: ["snippet", "preset"]),
SlashCommand(name: "Meeting", icon: "mic.fill", action: .meeting, section: "Suggested", keywords: ["record", "transcribe", "audio", "notes"]),
Expand Down Expand Up @@ -1087,6 +1120,9 @@ class BlockDocument {
if type == .heading || type == .headingToggle {
setHeadingLevel(id: blockId, level: headingLevel)
}
if type == .headingToggle {
autoNestIntoHeadingToggle(blockId: blockId, level: headingLevel)
}
}

dismissSlashMenu()
Expand Down
2 changes: 2 additions & 0 deletions Sources/Bugbook/Models/FileEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum TabKind: Equatable, Hashable, Codable {
case graphView
case skill
case gateway
case chat
case databaseRow(dbPath: String, rowId: String)

var isDatabase: Bool { self == .database }
Expand All @@ -18,6 +19,7 @@ enum TabKind: Equatable, Hashable, Codable {
var isGraphView: Bool { self == .graphView }
var isSkill: Bool { self == .skill }
var isGateway: Bool { self == .gateway }
var isChat: Bool { self == .chat }
var isDatabaseRow: Bool { if case .databaseRow = self { return true }; return false }
var databasePath: String? { if case .databaseRow(let p, _) = self { return p }; return nil }
var databaseRowId: String? { if case .databaseRow(_, let r) = self { return r }; return nil }
Expand Down
1 change: 1 addition & 0 deletions Sources/Bugbook/Models/OpenFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct OpenFile: Identifiable, Equatable, Codable {
var isGraphView: Bool { kind.isGraphView }
var isSkill: Bool { kind.isSkill }
var isGateway: Bool { kind.isGateway }
var isChat: Bool { kind.isChat }
var isDatabaseRow: Bool { kind.isDatabaseRow }
var databasePath: String? { kind.databasePath }
var databaseRowId: String? { kind.databaseRowId }
Expand Down
11 changes: 10 additions & 1 deletion Sources/Bugbook/Models/PaneContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ enum PaneContent: Codable, Equatable {
let id = UUID()
return .document(openFile: OpenFile(
id: id, path: "bugbook://gateway", content: "", isDirty: false, isEmptyTab: false,
kind: .gateway, displayName: "Gateway", icon: "square.grid.2x2"
kind: .gateway, displayName: "Home", icon: "square.grid.2x2"
))
}

/// A full-page chat pane.
static func chatDocument() -> PaneContent {
let id = UUID()
return .document(openFile: OpenFile(
id: id, path: "bugbook://chat", content: "", isDirty: false, isEmptyTab: false,
kind: .chat, displayName: "Chat", icon: "bubble.left.and.bubble.right"
))
}

Expand Down
16 changes: 16 additions & 0 deletions Sources/Bugbook/Models/WorkspaceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ private let log = Logger(subsystem: "com.bugbook.app", category: "WorkspaceManag
class WorkspaceManager {
var workspaces: [Workspace] = []
var activeWorkspaceIndex: Int = 0
/// Set after each successful layout save; UI can observe this for a brief indicator.
var lastSavedAt: Date?

@ObservationIgnored private var persistTask: Task<Void, Never>?

Expand Down Expand Up @@ -261,6 +263,19 @@ class WorkspaceManager {
activeWorkspace = ws
}

/// Swap the content of two panes by their IDs.
func swapPaneContents(paneA: UUID, paneB: UUID) {
guard var ws = activeWorkspace else { return }
guard let leafA = ws.root.findLeaf(id: paneA),
let leafB = ws.root.findLeaf(id: paneB) else { return }
let contentA = leafA.content
let contentB = leafB.content
ws.root = ws.root.updatingLeafContent(leafId: paneA, content: contentB)
ws.root = ws.root.updatingLeafContent(leafId: paneB, content: contentA)
activeWorkspace = ws
schedulePersist()
}

// MARK: - Queries

/// All document-type leaves across all workspaces.
Expand Down Expand Up @@ -322,6 +337,7 @@ class WorkspaceManager {
do {
let data = try JSONEncoder().encode(layout)
try data.write(to: Self.layoutFileURL, options: .atomic)
lastSavedAt = Date()
} catch {
log.error("Failed to persist workspace layout: \(error)")
}
Expand Down
30 changes: 21 additions & 9 deletions Sources/Bugbook/Services/TerminalManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@ final class TerminalManager {
func ensureInitialized() {
guard ghosttyApp == nil else { return }

// ghostty_init must be called before any other API call
let initResult = ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv)
// ghostty_init: pass empty args so libghostty doesn't touch the parent TTY
let initResult = ghostty_init(0, nil)
guard initResult == GHOSTTY_SUCCESS else {
log.error("ghostty_init failed with code \(initResult)")
return
}

// Create config
// Create config and load the user's Ghostty config (~/.config/ghostty/config)
guard let cfg = ghostty_config_new() else {
log.error("ghostty_config_new failed")
return
}
ghostty_config_load_default_files(cfg)
ghostty_config_finalize(cfg)
self.ghosttyConfig = cfg

Expand All @@ -50,13 +51,24 @@ final class TerminalManager {
// (new tab, new window, etc.). We don't support those yet.
return false
},
read_clipboard_cb: { _, _, _ in
// Read clipboard: ghostty wants to read the clipboard.
// Return false to indicate we don't support this yet.
return false
read_clipboard_cb: { userdata, clipboard, state in
// Must access NSPasteboard on the main thread
let work = {
guard let surface = _activeSurface else { return }
guard let text = NSPasteboard.general.string(forType: .string) else { return }
text.withCString { cStr in
ghostty_surface_complete_clipboard_request(surface, cStr, state, true)
}
}
if Thread.isMainThread { work() } else { DispatchQueue.main.async { work() } }
return true
},
confirm_read_clipboard_cb: { _, _, _, _ in
// Confirm clipboard read: no-op for now.
confirm_read_clipboard_cb: { userdata, content, state, requestType in
let work = {
guard let surface = _activeSurface, let content else { return }
ghostty_surface_complete_clipboard_request(surface, content, state, true)
}
if Thread.isMainThread { work() } else { DispatchQueue.main.async { work() } }
},
write_clipboard_cb: { userdata, loc, content, len, confirm in
// Write to system clipboard
Expand Down
Loading
Loading