Build a local desktop project manager for the Claude Code CLI.
The application must allow the user to:
- register and switch between local Git repositories;
- create, name, open, close, and resume multiple Claude Code sessions per repository;
- run the real locally installed
claudeCLI without bypassing its authentication, managed settings, environment variables, hooks, gateway configuration, or permission model; - browse repository files;
- preview source files;
- inspect Git status and diffs;
- keep multiple active Claude sessions open in tabs;
- resume inactive sessions without tmux or another terminal multiplexer;
- package the application as a self-contained macOS application image containing the application, Java runtime, JavaFX modules, native libraries, and all required JVM options.
The initial target is:
- macOS on Apple Silicon;
- JDK 26;
- JavaFX 26;
- libghostty embedded through the Foreign Function & Memory API;
- Gradle;
- a self-contained
jlinkapplication image; - a generated executable launcher containing all required JVM arguments;
- optional
.app/.dmgwrapping after the executable image is functional.
Do not implement a replacement Claude client. The application is a process and workspace manager around the official claude CLI.
Every interactive Claude session must be an actual invocation of the installed executable:
claudeor:
claude --resume '<session-id-or-name>'The application must:
- inherit the user environment;
- preserve the selected working directory;
- use the normal Claude Code settings hierarchy;
- use the normal Claude Code authentication;
- display Claude Code's original terminal UI;
- never send prompts directly to Anthropic;
- never reimplement Claude Code permissions;
- never parse terminal output to simulate a chat interface.
Do not introduce:
- tmux;
- screen;
- terminal multiplexing daemons;
- detached shells;
- long-lived external supervisor processes.
Each active session is a direct child process attached to a PTY managed by libghostty.
An inactive Claude session does not need a live process. Claude Code provides its own persistent session mechanism.
If the application cannot reliably determine a Claude session ID, it must still preserve the terminal session while active and allow resumption through:
claude --resumeDo not make undocumented Claude transcript formats part of the core process lifecycle.
All libghostty interaction must be isolated behind one Java package and, if required, one small native macOS host library.
No UI, project-management, Git, or persistence code may directly use:
MemorySegment;Linker;MethodHandle;- generated libghostty bindings;
- native pointers;
- AppKit handles.
The application must ship with its own JDK 26-derived runtime image built with jlink.
The user must not need:
- a separately installed JDK;
- a separately configured
JAVA_HOME; - Gradle;
- JavaFX;
- libghostty;
- any manually installed native application dependency other than the approved
claudeandgitexecutables.
The generated application launcher must include all required JVM arguments, including native-access flags.
- Add an existing local Git repository.
- Remove a repository from the application without deleting files.
- Display repositories in a sidebar.
- Create a new Claude session in a selected repository.
- Assign an application-visible session name.
- Run
claude -n '<name>'when supported. - Open several Claude sessions in tabs.
- Close an active terminal process.
- Resume a known session using
claude --resume. - Fall back to the official Claude session picker when no stable session identifier is known.
- Browse files in the repository.
- Preview text files with basic syntax highlighting.
- Open a file in the configured external editor.
- Display Git status.
- Display unified diffs.
- Persist project and UI metadata.
- Restore the previous application layout after restart.
- Build and run entirely from Gradle.
- Produce a self-contained executable application image using
jlink. - Produce a macOS
.appwrapper from that image. - Optionally produce a
.dmgafter the.appis stable.
- automatic Git worktree creation;
- Git staging and commits;
- embedded source editing;
- GitHub API integration;
- cloning repositories;
- remote SSH repositories;
- Windows support;
- Linux support;
- Intel macOS support;
- universal binaries;
- application auto-update;
- public notarized distribution;
- terminal process survival after application exit;
- background agents;
- custom Claude chat rendering;
- parsing all Claude JSONL transcript contents;
- moving sessions between repositories;
- running one Claude session simultaneously in several terminal tabs.
Do not build:
- a general IDE;
- another terminal emulator;
- another Git client;
- an Anthropic API client;
- a Claude Desktop replacement;
- an account or authentication system;
- a plugin marketplace;
- collaborative or cloud functionality.
┌─────────────────────────────────────────────────────────────┐
│ JavaFX application │
│ │
│ ┌─────────────┐ ┌─────────────────────────────────────────┐ │
│ │ Repository │ │ Workspace tabs │ │
│ │ sidebar │ │ │ │
│ │ │ │ ┌─────────────────────┬───────────────┐ │ │
│ │ repository │ │ │ Ghostty terminal │ File/Git pane │ │ │
│ │ ├ session │ │ │ running `claude` │ │ │ │
│ │ ├ session │ │ │ │ tree/preview │ │ │
│ │ └ session │ │ └─────────────────────┴───────────────┘ │ │
│ └─────────────┘ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Terminal abstraction │
│ │
│ TerminalView / TerminalSession / TerminalProcess │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Ghostty adapter │
│ │
│ Generated FFM bindings + handwritten lifecycle wrapper │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Bundled libghostty + optional AppKit host shim │
└─────────────────────────────────────────────────────────────┘
Packaging:
Gradle build
│
├── compile application modules
├── compile/copy native libraries
├── build custom runtime with jlink
├── generate executable launcher
└── assemble app image
├── bin/
├── runtime/
├── app/
└── lib/
Use a Gradle multi-project build so the native boundary remains isolated.
drydock/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
├── app/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/java/
│ │ └── app/drydock/
│ ├── main/resources/
│ │ ├── app.css
│ │ ├── icons/
│ │ └── syntax/
│ └── test/java/
├── terminal-api/
│ ├── build.gradle.kts
│ └── src/main/java/
├── terminal-ghostty/
│ ├── build.gradle.kts
│ ├── src/main/java/
│ ├── src/generated/java/
│ └── src/test/java/
├── native-host/
│ ├── CMakeLists.txt or build.zig
│ └── src/
│ ├── terminal_host.h
│ └── terminal_host.m
├── third_party/
│ └── ghostty/
├── packaging/
│ ├── macos/
│ │ ├── Info.plist
│ │ ├── entitlements.plist
│ │ ├── launcher-template.sh
│ │ └── app.icns
│ └── scripts/
│ ├── assemble-app-image.sh
│ ├── create-macos-app.sh
│ └── create-dmg.sh
├── docs/
│ ├── architecture.md
│ ├── native-integration.md
│ ├── session-model.md
│ └── packaging.md
└── scripts/
├── verify-environment.sh
├── build-ghostty.sh
└── smoke-test-app.sh
The project may initially remain a single Gradle module during the feasibility spike. Split it into the modules above once the terminal prototype works.
Use JDK 26.
Configure Gradle toolchains:
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(26))
}
}Fail the build with a clear message when JDK 26 is unavailable.
Use JavaFX 26 modules:
javafx.base;javafx.controls;javafx.graphics.
Avoid FXML initially. Build views in Java so generated UI changes remain easy to trace and refactor.
Add javafx.fxml only if it produces a concrete benefit later.
Use Gradle Kotlin DSL.
Required top-level commands:
./gradlew run
./gradlew test
./gradlew integrationTest
./gradlew runtimeImage
./gradlew appImage
./gradlew macApp
./gradlew dmgThe exact internal task names may differ, but provide these aliases.
Launch the application with:
--enable-native-access=app.drydock.terminal.ghostty
Use ALL-UNNAMED only during the earliest non-modular spike.
Prefer a modular application once native loading is stable.
Use jlink to build the application runtime image.
The image must contain:
- the JDK modules required by the application;
- JavaFX modules;
- the application modules;
- generated launch scripts;
- all required JVM arguments;
- application resources;
- bundled native libraries.
The launcher must carry JVM arguments such as:
--enable-native-access=app.drydock.terminal.ghostty
-Djava.library.path=$APP_HOME/lib
-Dfile.encoding=UTF-8
-Djava.awt.headless=false
Add only arguments that are actually required and verified.
Prefer a Gradle plugin capable of generating a jlink application image and launch scripts, or implement the jlink command explicitly if the plugin obscures the generated layout.
The build must make the launcher reproducible and inspectable.
Use SQLite or a small atomic JSON store.
Prefer SQLite if session and repository queries become nontrivial. Prefer JSON for the first spike.
Default location:
~/Library/Application Support/ClaudeProjectManager/
Suggested contents:
state.db
logs/
cache/
Application metadata must never be stored inside user repositories unless explicitly enabled.
Invoke the installed git executable.
Do not introduce JGit initially.
Use commands such as:
git -C <repo> rev-parse --show-toplevel
git -C <repo> status --porcelain=v2 --branch -z
git -C <repo> diff --no-ext-diff --no-color
git -C <repo> diff --cached --no-ext-diff --no-color
git -C <repo> worktree list --porcelainPass arguments as a process argument list. Never construct a shell command by concatenating paths.
Use the installed claude executable.
At startup, discover it using:
- explicitly configured path;
- inherited
PATH; - common installation paths only as a convenience fallback.
Run:
claude --versionStore the detected executable path and version.
Do not assume every recent flag is supported. Introduce capability detection based on:
claude --helpRepresent capabilities explicitly:
record ClaudeCapabilities(
boolean supportsName,
boolean supportsResume,
boolean supportsForkSession,
String version
) {}Do not build the complete application before these gates pass.
Pin Ghostty to an exact commit.
The build must:
- initialize the submodule;
- build libghostty for macOS arm64;
- produce a dylib or static library suitable for application embedding;
- copy public headers to a deterministic build directory;
- record the Ghostty commit in generated build metadata.
Deliverables:
build/native/macos-arm64/libghostty.dylib
build/native/include/ghostty.h
build/generated/ghostty-version.properties
Acceptance criteria:
- clean checkout can build the library with one Gradle command;
- no manually installed files are required outside documented build prerequisites;
- rebuilding without source changes is incremental;
- failure output clearly identifies missing Zig/Xcode dependencies.
Create a small Java command-line program that:
- loads the built libghostty library using FFM;
- invokes a harmless version, initialization, or configuration function;
- validates a returned value;
- shuts down without a crash.
Acceptance criteria:
./gradlew :terminal-ghostty:ffmSmokeTestmust exit successfully.
Create the smallest possible JavaFX window that embeds one Ghostty terminal surface.
The spike must establish which model is viable:
Libghostty renders into a native macOS view or layer hosted inside the JavaFX window.
Libghostty exposes a caller-managed render buffer that JavaFX can display efficiently.
Do not commit to the alternative until its API actually exists and works at the pinned Ghostty commit.
Acceptance criteria:
- a window opens;
- terminal content is rendered;
- window resizing updates terminal dimensions;
- focus works;
- keyboard input reaches the terminal;
- the application closes without a crash.
Spawn:
/bin/zsh -linside the embedded terminal.
Verify:
- prompt rendering;
- ordinary typing;
- Return;
- Backspace;
- arrow keys;
- Ctrl+C;
- Ctrl+D;
- Option-based key combinations;
- copy and paste;
- selection;
- terminal resizing;
- scrollback;
- Unicode;
- wide characters;
- colour output;
- alternate-screen applications;
vimor another full-screen TUI;- process exit.
Run the real CLI in a test repository:
claudeVerify:
- startup;
- workspace trust prompt;
- login/configuration behaviour;
- multiline prompt editing;
- permission prompts;
- streaming output;
- tool execution display;
- Ctrl+C cancellation;
- terminal resizing during output;
- copy/paste;
- quitting and relaunching;
claude --resume;- inherited managed settings;
- inherited gateway environment variables.
Build the first jlink image containing:
- the application module;
- JavaFX modules;
- FFM usage;
- generated launcher;
- required JVM arguments;
- bundled libghostty.
Acceptance criteria:
./gradlew runtimeImage
build/image/bin/drydockmust launch the terminal spike without:
- an external JDK;
- Gradle;
JAVA_HOME;- native libraries outside the image.
Proceed with the full application only if Gates 0A–0F pass reliably.
If native-view embedding fails, do not immediately abandon JavaFX. Implement the minimal AppKit host shim described below.
Implement this only if JavaFX cannot directly provide the native host expected by libghostty.
The shim must be intentionally small.
Suggested API:
typedef void *drydock_terminal_host_t;
drydock_terminal_host_t drydock_terminal_host_create(
void *parent_nsview
);
void drydock_terminal_host_set_frame(
drydock_terminal_host_t host,
double x,
double y,
double width,
double height
);
void *drydock_terminal_host_content_view(
drydock_terminal_host_t host
);
void drydock_terminal_host_set_visible(
drydock_terminal_host_t host,
bool visible
);
void drydock_terminal_host_set_focused(
drydock_terminal_host_t host,
bool focused
);
void drydock_terminal_host_destroy(
drydock_terminal_host_t host
);Responsibilities:
- create and destroy an AppKit child view;
- attach it to the JavaFX window's native view hierarchy;
- update frame coordinates;
- support visibility changes;
- support focus transfer;
- expose whatever view/layer handle libghostty requires.
It must not:
- spawn Claude;
- manage sessions;
- store application state;
- parse keyboard shortcuts;
- implement terminal rendering;
- implement file browsing;
- know about repositories.
All shim calls must occur on the appropriate macOS UI thread.
Document JavaFX-thread/AppKit-thread interactions explicitly.
Create a terminal API that hides Ghostty.
public interface TerminalView extends AutoCloseable {
Node node();
TerminalSession start(TerminalLaunchRequest request);
void requestFocus();
void setVisible(boolean visible);
@Override
void close();
}public interface TerminalSession extends AutoCloseable {
UUID id();
ProcessState state();
OptionalLong pid();
CompletionStage<Integer> exitCode();
void resize(TerminalSize size);
void sendText(String text);
void sendInterrupt();
void terminate();
@Override
void close();
}public record TerminalLaunchRequest(
Path executable,
List<String> arguments,
Path workingDirectory,
Map<String, String> environment,
String displayName
) {}public enum ProcessState {
STARTING,
RUNNING,
EXITED,
FAILED,
TERMINATING
}Do not expose libghostty pointers or callbacks through this API.
- Every terminal surface has one owning Java object.
- Native resources are explicitly closed.
Cleanermay be used only as a safety net, not as the normal lifecycle.- Closing a tab prompts before terminating a running process.
- Application shutdown prompts once for all active processes.
- A force-quit path must exist.
- Callback stubs remain strongly reachable while native code can call them.
- No callback may update JavaFX controls off the JavaFX application thread.
- Native callback exceptions must be caught and logged before returning across the FFM boundary.
record RepositoryId(UUID value) {}
record Repository(
RepositoryId id,
Path root,
String displayName,
Instant addedAt,
Instant lastOpenedAt,
RepositorySettings settings
) {}Repository invariants:
rootis absolute and normalized;- repository existence is revalidated when opened;
- symlink behaviour is explicit;
- Git root is detected with
git rev-parse; - duplicate repositories resolve by canonical root path;
- removing a repository removes only application metadata.
record ManagedSessionId(UUID value) {}
record ManagedClaudeSession(
ManagedSessionId id,
RepositoryId repositoryId,
String displayName,
Optional<String> claudeSessionId,
Optional<String> claudeSessionName,
Path workingDirectory,
Optional<Path> worktreeRoot,
SessionStatus status,
Instant createdAt,
Instant lastOpenedAt,
Optional<Integer> lastExitCode
) {}enum SessionStatus {
INACTIVE,
STARTING,
RUNNING,
EXITED,
FAILED,
MISSING_WORKING_DIRECTORY
}Keep these identifiers distinct:
- application session ID;
- Claude Code session ID;
- Claude Code session name;
- OS process ID.
Never overload one field to represent another.
Persist:
- selected repository;
- selected session;
- open tabs;
- sidebar width;
- right-pane width;
- last selected file;
- expanded repository nodes;
- file-preview visibility;
- Git-pane visibility.
Do not attempt to persist the live native terminal surface.
When the user creates a session:
- Require a repository.
- Generate a default display name.
- Allow immediate renaming.
- Create the application session metadata.
- Start:
claude -n '<name>'when the installed version supports --name.
Otherwise start:
claude- Use the repository root or selected worktree as the working directory.
- Inherit the application environment.
- Add only application-specific environment variables that are strictly necessary.
- Mark the session running after successful process creation.
Do not pass permission-bypass flags.
If a trusted Claude session ID is known:
claude --resume '<session-id>'If only an explicitly assigned Claude session name is known:
claude --resume '<name>'If neither is known:
claude --resumeThis opens the official session picker.
Always launch from the stored working directory.
If that directory no longer exists:
- do not silently switch to another repository;
- mark the session as missing its directory;
- allow the user to choose a replacement;
- record the reassignment explicitly.
Do not open the same Claude session in two tabs by default.
Maintain an application-level map:
Claude session ID -> active terminal tab
If the user tries to open an already active session:
- focus the existing tab;
- provide an explicit “Open anyway” advanced action only later.
Use official CLI-supported identifiers whenever possible.
Do not initially depend on parsing private JSONL formats to discover the session ID of a newly created interactive terminal.
Implement session identification in stages:
Application-managed names plus claude --resume <name>.
Optional discovery from supported Claude commands, if a stable machine-readable command exists in the installed version.
Read-only local transcript discovery under ~/.claude/projects/, isolated behind a ClaudeSessionDiscovery interface.
Private file parsing must never be required to launch or resume through the official picker.
Use a two-level tree initially:
java-profiler
pthread cleanup
allocation sampling
JFR context
jdk
jmethodID investigation
Repository row:
- display name;
- current branch;
- dirty indicator;
- number of running sessions.
Session row:
- session name;
- status icon;
- running/idle/failed state;
- optional branch or worktree;
- last-opened time in tooltip.
Context actions:
Repository:
- New Claude session;
- Refresh;
- Open in Finder;
- Open in external editor;
- Remove from manager.
Session:
- Open;
- Resume;
- Rename;
- Stop process;
- Reveal working directory;
- Remove application entry.
Do not delete Claude transcripts from the first version.
Use a SplitPane.
Left/main section:
- one tab per open managed session;
- each tab hosts one Ghostty terminal view;
- tab title displays session name;
- dirty/running/attention state may be reflected with a small icon.
Right section:
- Files;
- Git;
- optionally Session Info.
Suggested structure:
TabPane: terminal tabs
SplitPane:
terminal area
inspector area:
TabPane:
Files
Git
Session
Do not create one JavaFX Stage per terminal initially.
Load directories lazily.
Ignore by default:
.git
.gradle
.idea
build
out
target
node_modules
Do not hardcode the ignore logic permanently. Create:
interface FileVisibilityPolicy {
boolean isVisible(Path path);
}Potential later inputs:
.gitignore;- application settings;
- user exclusions.
Use WatchService carefully.
Do not recursively register the entire repository synchronously at startup.
Version 0.1 may use:
- refresh on focus;
- refresh after known Git operations;
- explicit refresh button;
- watchers only for expanded directories.
Debounce refresh events.
Support:
- plain text;
- Java;
- Kotlin;
- C;
- C++;
- headers;
- Python;
- shell;
- JSON;
- YAML;
- TOML;
- Markdown;
- Gradle files;
- Git diff.
For large files:
- define a configurable threshold, initially 2 MiB;
- show metadata instead of automatically loading;
- provide an explicit “Open anyway” action.
For binary files:
- show file type and size;
- do not attempt text decoding.
Initial preview can use a non-editable text control. Introduce RichTextFX only if needed for practical syntax highlighting or performance.
Provide a configurable command template.
Examples:
code --goto {file}:{line}:{column}
idea --line {line} {file}
open -a "IntelliJ IDEA" {file}
Execute without a shell whenever possible.
Parse:
git status --porcelain=v2 --branch -zRepresent:
record GitStatus(
String branch,
Optional<String> upstream,
int ahead,
int behind,
List<GitFileChange> changes
) {}Group changes:
- staged;
- unstaged;
- untracked;
- conflicts.
Selecting a changed file displays its diff.
Use:
git diff --no-ext-diff --no-color -- <path>
git diff --cached --no-ext-diff --no-color -- <path>Protect against:
- unusual filenames;
- spaces;
- newlines in paths;
- deleted files;
- renamed files;
- binary files;
- repositories with no commits.
Refresh:
- when the Git tab becomes visible;
- after a terminal process exits;
- after filesystem changes, debounced;
- through an explicit refresh action.
Do not poll aggressively.
A macOS GUI application may not inherit the same PATH as an interactive shell.
Implement explicit environment diagnosis.
At startup display or log:
- application
PATH; - detected
claudepath; - detected
gitpath; - detected shell;
claude --version;git --version;- Java version;
- application architecture;
- libghostty commit.
Provide a settings screen allowing explicit executable paths.
Do not silently run the application through:
zsh -lcfor every child process.
If login-shell environment import is required, implement it once as an explicit optional feature:
/usr/bin/env -i HOME="$HOME" /bin/zsh -l -c 'env -0'Parse the result and show the user what was imported.
Never log secrets or token values.
Create a repository abstraction:
interface ApplicationStateRepository {
ApplicationState load();
void save(ApplicationState state);
}Required properties:
- atomic writes;
- schema version;
- migration support;
- backup of the previous state;
- recovery from truncated or malformed state;
- no corruption if the application crashes during save.
If using JSON:
- write to a temporary file;
- fsync where practical;
- atomically rename;
- retain one backup.
If using SQLite:
- enable WAL;
- use transactions;
- include a schema-version table;
- keep migrations deterministic.
Owns:
- UI controls;
- observable UI state;
- view creation and destruction;
- visible selection state.
AppKit view operations must run on the macOS main thread as required.
If this is the same native thread used by JavaFX, document the verified mechanism. Do not assume.
Use virtual threads or a bounded executor for:
- Git commands;
- Claude capability detection;
- file loading;
- metadata persistence;
- process waits;
- directory scanning.
Never block the JavaFX application thread on:
Process.waitFor;- Git;
- filesystem traversal;
- transcript scanning;
- native terminal process completion.
Native callbacks must:
- validate arguments;
- copy transient native data before returning where required;
- catch all Java exceptions;
- enqueue UI work through
Platform.runLater; - avoid blocking libghostty's rendering or I/O threads.
Write logs to:
~/Library/Logs/ClaudeProjectManager/
Include:
- application startup;
- Java and JavaFX version;
- architecture;
- native library path;
- Ghostty commit;
- Claude executable and version;
- repository registration;
- process launches without secret environment values;
- process exits;
- native lifecycle operations;
- uncaught exceptions.
Provide:
- “Open Logs” action;
- “Copy Diagnostic Summary” action.
Diagnostic summary must redact:
- API keys;
- authorization headers;
- tokens;
- full environment;
- prompt content;
- session transcript content.
Use user-visible errors that state:
- what failed;
- which executable or path was involved;
- the exit code;
- the relevant stderr excerpt;
- the recovery action.
Examples:
- Claude executable not found.
- Git executable not found.
- Repository path no longer exists.
- Directory is not a Git repository.
- Claude process could not start.
- Native terminal could not initialize.
- Session cannot be found in this working directory.
- Native library architecture mismatch.
- Application lacks permission to access the directory.
Never reduce these to “Something went wrong.”
- Do not expose an HTTP server.
- Do not listen on network ports.
- Do not add telemetry.
- Do not upload logs.
- Do not inspect or copy authentication tokens.
- Do not modify managed Claude settings.
- Do not bypass Claude permissions.
- Do not pass
--dangerously-skip-permissions. - Do not run repository paths through a shell string.
- Treat repository filenames as untrusted input.
- Never interpret file content as application configuration without explicit opt-in.
- Keep terminal process environment inheritance predictable.
- Do not persist full prompts or terminal scrollback unless explicitly added later.
Cover:
- repository path normalization;
- duplicate-repository detection;
- application-state serialization;
- state migration;
- Claude capability parsing;
- process argument construction;
- Git porcelain parsing;
- filenames with spaces and unusual characters;
- session lifecycle state transitions;
- executable discovery;
- environment redaction.
Use temporary Git repositories.
Cover:
- clean repository;
- modified tracked file;
- staged file;
- untracked file;
- renamed file;
- deleted file;
- merge conflict;
- repository with no commits;
- Git worktree;
- repository path containing spaces;
- repository path containing Unicode.
Cover:
- native library load;
- Ghostty initialization;
- terminal create/destroy loop;
- repeated tab creation and closure;
- process start and exit;
- resize storm;
- focus changes;
- application shutdown with active sessions;
- callback invocation after rapid closure;
- absence of callbacks after resource destruction.
Run a stress test that creates and destroys at least 100 terminal surfaces sequentially.
Maintain a checked-in checklist covering:
- shell;
- Claude Code;
- Vim;
- coloured output;
- Unicode;
- emoji;
- selection;
- clipboard;
- resizing;
- alternate screen;
- Ctrl+C;
- Ctrl+D;
- Cmd+C;
- Cmd+V;
- Option+arrow;
- Home/End;
- Page Up/Page Down;
- application tab switching;
- application hide/show;
- sleep/wake;
- external display disconnect;
- Retina scaling.
On a clean macOS user account:
- build the runtime image;
- copy it outside the Gradle build directory;
- launch it without a system JDK;
- add a repository;
- start
/bin/zsh; - start Claude;
- close and reopen the application;
- resume the session.
The image must not rely on:
JAVA_HOME;- Gradle;
- source-tree-relative paths;
- native libraries outside the image.
Use jlink as the primary packaging mechanism.
The generated image must include:
ClaudeProjectManager/
├── bin/
│ └── drydock
├── runtime/
│ ├── bin/
│ ├── conf/
│ ├── legal/
│ └── lib/
├── app/
│ ├── app modules/jars
│ └── resources
└── lib/
├── libghostty.dylib
└── libdrydock_terminal_host.dylib
The exact layout may differ, but it must be deterministic and documented.
Generate an executable launcher that:
- resolves its installation directory without depending on the current working directory;
- launches the bundled Java executable;
- sets module path and main module;
- applies required JVM options;
- points native library loading at bundled libraries;
- preserves user arguments;
- returns the application exit code.
Required JVM options must be embedded in build configuration rather than supplied manually by the user.
Example:
--enable-native-access=app.drydock.terminal.ghostty
-Dfile.encoding=UTF-8
-Djava.library.path=<image>/lib
Do not add speculative JVM flags.
After the raw executable image works, wrap it in:
Drydock.app/
└── Contents/
├── Info.plist
├── MacOS/
│ └── Drydock
├── runtime/
├── app/
├── Frameworks/
│ ├── libghostty.dylib
│ └── libdrydock_terminal_host.dylib
└── Resources/
The .app launcher may invoke the bundled jlink launcher or be generated directly from the same launcher metadata.
Ensure native library lookup does not depend on:
DYLD_LIBRARY_PATH
Use loader-relative paths such as @rpath or @loader_path where appropriate.
Run from Gradle with native libraries in the build directory.
Produce a raw jlink executable image.
Produce an unsigned .app.
Produce a local .dmg.
Ad hoc sign nested native components for local Gatekeeper sanity.
Add Developer ID signing and notarization only when external distribution is required.
All nested native libraries and executables must be signed in the correct order before signing the outer application bundle.
Pin:
- JDK distribution/version;
- JavaFX version;
- Gradle version;
- Ghostty Git commit;
- Zig version if required;
- native host build tooling.
Generate a build information file:
application.version=...
git.commit=...
build.timestamp=...
jdk.version=26
javafx.version=26.x
ghostty.commit=...
target.os=macos
target.arch=aarch64Expose this through an About dialog and diagnostic summary.
Do not download unversioned “latest” native artifacts.
Deliver:
- Gradle wrapper;
- JDK 26 toolchain;
- JavaFX application with one empty window;
- test infrastructure;
- environment verification script;
- basic CI compile/test job on macOS arm64 where available.
Exit criteria:
./gradlew clean test runworks.
Deliver:
- pinned Ghostty source;
- native build task;
- FFM bindings;
- FFM smoke test;
- one terminal surface in JavaFX;
- interactive shell.
Exit criteria:
All Phase 0 gates except Claude and runtime-image packaging pass.
Deliver:
- executable discovery;
- environment inheritance;
- Claude launch;
- Claude resume;
- terminal keyboard/clipboard correctness;
- manual compatibility report.
Exit criteria:
A real Claude Code session is usable for normal work inside the application.
Do not proceed if this remains flaky.
Deliver:
- modular application layout;
- custom runtime image;
- generated launcher;
- embedded JVM arguments;
- bundled JavaFX;
- bundled libghostty;
- bundled native host shim if required;
- clean-user smoke test.
Exit criteria:
The terminal spike and Claude Code launch from the generated image without an installed JDK.
Deliver:
- add/remove repository;
- persistence;
- repository sidebar;
- branch and dirty indicators;
- open in Finder/editor.
Exit criteria:
Repositories survive application restart and refresh correctly.
Deliver:
- new session;
- session naming;
- terminal tabs;
- process state;
- stop/close;
- resume;
- duplicate-open prevention;
- persisted session metadata.
Exit criteria:
At least three Claude sessions can be opened across at least two repositories and resumed after application restart.
Deliver:
- lazy file tree;
- preview;
- binary/large-file handling;
- external-editor integration;
- refresh behaviour.
Exit criteria:
The repository can be navigated comfortably without leaving the application.
Deliver:
- Git status;
- grouped changes;
- staged and unstaged diff;
- branch/ahead/behind;
- refresh.
Exit criteria:
Git changes made by Claude appear reliably without restarting the application.
Deliver:
.app;- optional
.dmg; - native-library bundling;
- local signing;
- clean-account smoke test;
- packaging documentation.
Exit criteria:
The application runs from the packaged bundle without Gradle or an installed JDK.
Deliver:
- stress tests;
- shutdown handling;
- crash recovery;
- state backup;
- diagnostics;
- performance profiling;
- sleep/wake testing;
- external-display testing.
Exit criteria:
The application is reliable enough to replace separate terminal windows for daily work.
- Application window visible within 3 seconds on a warm start.
- Repository sidebar restored within 1 second after window display.
- Terminal input latency not perceptibly worse than Ghostty itself.
- File selection preview under 200 ms for ordinary source files.
- Git status refresh under 1 second for normal repositories.
- No full recursive repository scan on the JavaFX application thread.
- Idle application CPU near zero.
- Hidden terminal tabs must not continuously repaint.
- Memory must return close to baseline after repeatedly opening and closing terminal tabs.
Record baseline measurements after Milestone 2.
- Work milestone by milestone.
- Do not scaffold later milestones before the current milestone works.
- Keep the application runnable after every meaningful commit.
- Run tests after each structural change.
- Do not replace failing native integration with mocked success.
- Do not invent libghostty APIs. Inspect the pinned headers and examples.
- Do not assume JavaFX exposes an
NSViewthrough a public API. Verify it. - Do not use internal JavaFX APIs without documenting the exact dependency and failure risk.
- Prefer a tiny explicit native host shim over extensive reflection into JavaFX internals.
- Do not parse private Claude session files until basic launch and resume work through official CLI commands.
- Never change Claude authentication or managed-settings files.
- Never use a shell where direct process arguments are sufficient.
- Add cleanup and ownership logic when native objects are introduced, not later.
- Add a focused regression test for every native crash or lifecycle bug.
- Record unresolved risks in
docs/architecture.md. - Before introducing a dependency, explain what problem it solves and why existing dependencies cannot solve it.
- Keep generated FFM sources separate from handwritten code.
- Make generated bindings reproducible from the pinned header.
- Treat warnings from native compilation, FFM,
jlink, and packaging as errors where practical. - Do not claim a milestone complete until its exit criteria have been manually or automatically verified.
- Treat the
jlinkruntime image as the canonical deployable artifact. - Keep JVM arguments in version-controlled build configuration.
- Make the generated launcher inspectable and reproducible.
- Do not require users to install or select a JDK.
- Do not introduce
jpackageas the core runtime builder; it may be used only as an optional wrapper around the already workingjlinkimage.
Begin with the following tasks only.
Create the Gradle JDK 26 and JavaFX 26 project.
Add:
- application entry point;
- one JavaFX window;
- unit-test setup;
- Gradle wrapper;
- environment verification;
- README with exact development prerequisites.
Add Ghostty as a pinned Git submodule.
Determine from the pinned repository:
- supported libghostty build command;
- produced native artifacts;
- public C headers;
- required macOS frameworks;
- architecture settings;
- embedding examples;
- lifecycle and thread constraints.
Write findings to:
docs/native-integration.md
Do not guess.
Create a Gradle task that builds libghostty for macOS arm64.
Make the output deterministic and place it under build/native.
Generate or handwrite the smallest FFM binding needed to initialize libghostty.
Run it from a command-line smoke test.
Investigate native surface embedding.
Produce a minimal JavaFX terminal window with no repository manager or Claude-specific code.
Run /bin/zsh -l.
Complete the terminal interaction checklist.
Run the installed claude CLI in a temporary test repository.
Document every incompatibility before proceeding.
Create the first jlink runtime image.
The image must:
- bundle Java 26 runtime modules;
- bundle JavaFX;
- bundle the application;
- bundle libghostty;
- include the native-access JVM option;
- run without
JAVA_HOME; - run outside the source tree.
Stop after Task 8 and report:
- what works;
- what does not;
- whether an AppKit shim was required;
- what undocumented or unstable APIs are being used;
- native ownership rules;
- packaging implications;
- exact generated runtime-image layout;
- exact launcher JVM arguments;
- whether the architecture remains viable.
Do not implement project management until this report is complete.
Version 0.1 is complete when:
- the self-contained runtime image launches without an installed JDK;
- the macOS
.appwrapper launches the same image; - the user can register multiple Git repositories;
- each repository can contain multiple managed Claude sessions;
- several sessions can be active in terminal tabs;
- inactive sessions can be resumed in their correct repository or worktree;
- repository files can be browsed and previewed;
- Git changes and diffs can be inspected;
- Claude runs exclusively through the local CLI;
- no tmux or external terminal multiplexer is involved;
- closing and reopening the app does not lose application-managed session metadata;
- all required JVM arguments are embedded in the generated launcher;
- no known reproducible native crash remains in normal session creation, resizing, tab switching, or shutdown;
- the build and packaging process is documented and reproducible.