Include local changes

This commit is contained in:
Lawrence Chen 2026-01-30 16:46:26 -08:00
parent 4b77b1117d
commit e234123c17
5 changed files with 223 additions and 2 deletions

4
.gitignore vendored
View file

@ -14,8 +14,8 @@ xcuserdata/
# Swift Package Manager
.swiftpm/
# GhosttyKit binary (rebuild from /tmp/ghostty with zig build)
GhosttyKit.xcframework/
# GhosttyKit binary (built from ghostty submodule via scripts/setup.sh)
GhosttyKit.xcframework
GhosttyKit.xcframework.bak-*/
# Release artifacts

View file

@ -1,5 +1,13 @@
# cmuxterm agent notes
## Initial setup
Run the setup script to initialize submodules and build GhosttyKit:
```bash
./scripts/setup.sh
```
## Local dev
After making code changes, always run the reload script to launch the Debug app:

98
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,98 @@
# Contributing to cmuxterm
## Prerequisites
- macOS 14+
- Xcode 15+
- [Zig](https://ziglang.org/) (install via `brew install zig`)
## Getting Started
1. Clone the repository with submodules:
```bash
git clone --recursive https://github.com/manaflow-ai/cmuxterm.git
cd cmuxterm
```
2. Run the setup script:
```bash
./scripts/setup.sh
```
This will:
- Initialize git submodules (ghostty, homebrew-cmuxterm)
- Build the GhosttyKit.xcframework from source
- Create the necessary symlinks
3. Build and run the debug app:
```bash
./scripts/reload.sh
```
## Development Scripts
| Script | Description |
|--------|-------------|
| `./scripts/setup.sh` | One-time setup (submodules + xcframework) |
| `./scripts/reload.sh` | Build and launch Debug app |
| `./scripts/reloadp.sh` | Build and launch Release app |
| `./scripts/reload2.sh` | Reload both Debug and Release |
| `./scripts/rebuild.sh` | Clean rebuild |
## Rebuilding GhosttyKit
If you make changes to the ghostty submodule, rebuild the xcframework:
```bash
cd ghostty
zig build -Demit-xcframework=true -Doptimize=ReleaseFast
```
## Running Tests
### Basic tests (run on VM)
```bash
ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" build && pkill -x "cmuxterm DEV" || true && APP=$(find /Users/cmux/Library/Developer/Xcode/DerivedData -path "*/Build/Products/Debug/cmuxterm DEV.app" -print -quit) && open "$APP" && for i in {1..20}; do [ -S /tmp/cmuxterm.sock ] && break; sleep 0.5; done && python3 tests/test_update_timing.py && python3 tests/test_signals_auto.py && python3 tests/test_ctrl_socket.py && python3 tests/test_notifications.py'
```
### UI tests (run on VM)
```bash
ssh cmux-vm 'cd /Users/cmux/GhosttyTabs && xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug -destination "platform=macOS" -only-testing:GhosttyTabsUITests test'
```
## Ghostty Submodule
The `ghostty` submodule points to [manaflow-ai/ghostty](https://github.com/manaflow-ai/ghostty), a fork of the upstream Ghostty project.
### Making changes to ghostty
```bash
cd ghostty
git checkout -b my-feature
# make changes
git add .
git commit -m "Description of changes"
git push manaflow my-feature
```
### Keeping the fork updated
```bash
cd ghostty
git fetch origin
git checkout main
git merge origin/main
git push manaflow main
```
Then update the parent repo:
```bash
cd ..
git add ghostty
git commit -m "Update ghostty submodule"
```
See `docs/ghostty-fork.md` for details on fork changes and conflict notes.

View file

@ -274,6 +274,9 @@ class TabManager: ObservableObject {
let previousSurfaceId = focusedSurfaceId(for: previousTabId) {
lastFocusedSurfaceByTab[previousTabId] = previousSurfaceId
}
if !isNavigatingHistory, let selectedTabId {
recordTabInHistory(selectedTabId)
}
DispatchQueue.main.async { [weak self] in
self?.focusSelectedTabSurface(previousTabId: previousTabId)
self?.updateWindowTitleForSelectedTab()
@ -287,6 +290,12 @@ class TabManager: ObservableObject {
private var suppressFocusFlash = false
private var lastFocusedSurfaceByTab: [UUID: UUID] = [:]
// Recent tab history for back/forward navigation (like browser history)
private var tabHistory: [UUID] = []
private var historyIndex: Int = -1
private var isNavigatingHistory = false
private let maxHistorySize = 50
init() {
addTab()
observers.append(NotificationCenter.default.addObserver(
@ -687,6 +696,82 @@ class TabManager: ObservableObject {
selectedTabId = lastTab.id
}
// MARK: - Recent Tab History Navigation
private func recordTabInHistory(_ tabId: UUID) {
// If we're not at the end of history, truncate forward history
if historyIndex < tabHistory.count - 1 {
tabHistory = Array(tabHistory.prefix(historyIndex + 1))
}
// Don't add duplicate consecutive entries
if tabHistory.last == tabId {
return
}
tabHistory.append(tabId)
// Trim history if it exceeds max size
if tabHistory.count > maxHistorySize {
tabHistory.removeFirst(tabHistory.count - maxHistorySize)
}
historyIndex = tabHistory.count - 1
}
func navigateBack() {
guard historyIndex > 0 else { return }
// Find the previous valid tab in history (skip closed tabs)
var targetIndex = historyIndex - 1
while targetIndex >= 0 {
let tabId = tabHistory[targetIndex]
if tabs.contains(where: { $0.id == tabId }) {
isNavigatingHistory = true
historyIndex = targetIndex
selectedTabId = tabId
isNavigatingHistory = false
return
}
// Remove closed tab from history
tabHistory.remove(at: targetIndex)
historyIndex -= 1
targetIndex -= 1
}
}
func navigateForward() {
guard historyIndex < tabHistory.count - 1 else { return }
// Find the next valid tab in history (skip closed tabs)
let targetIndex = historyIndex + 1
while targetIndex < tabHistory.count {
let tabId = tabHistory[targetIndex]
if tabs.contains(where: { $0.id == tabId }) {
isNavigatingHistory = true
historyIndex = targetIndex
selectedTabId = tabId
isNavigatingHistory = false
return
}
// Remove closed tab from history
tabHistory.remove(at: targetIndex)
// Don't increment targetIndex since we removed the element
}
}
var canNavigateBack: Bool {
historyIndex > 0 && tabHistory.prefix(historyIndex).contains { tabId in
tabs.contains { $0.id == tabId }
}
}
var canNavigateForward: Bool {
historyIndex < tabHistory.count - 1 && tabHistory.suffix(from: historyIndex + 1).contains { tabId in
tabs.contains { $0.id == tabId }
}
}
func newSplit(tabId: UUID, surfaceId: UUID, direction: SplitTree<TerminalSurface>.NewDirection) -> Bool {
guard let tab = tabs.first(where: { $0.id == tabId }) else { return false }
return tab.newSplit(from: surfaceId, direction: direction) != nil

30
scripts/setup.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
echo "==> Initializing submodules..."
git submodule update --init --recursive
echo "==> Checking for zig..."
if ! command -v zig &> /dev/null; then
echo "Error: zig is not installed."
echo "Install via: brew install zig"
exit 1
fi
echo "==> Building GhosttyKit.xcframework (this may take a few minutes)..."
cd ghostty
zig build -Demit-xcframework=true -Doptimize=ReleaseFast
cd "$PROJECT_DIR"
echo "==> Creating symlink for GhosttyKit.xcframework..."
ln -sf ghostty/macos/GhosttyKit.xcframework GhosttyKit.xcframework
echo "==> Setup complete!"
echo ""
echo "You can now build and run the app:"
echo " ./scripts/reload.sh"