Skip to content

Commit 8e4df68

Browse files
Add pair programming plugin (#43)
* ADFA-4395: Update name of NDK plugin to indicate 64bit ONLY * Add pair programming plugin * ADFA-4395: Bundle shared.jar inside the pair plugin pair depends on com.itsaky.androidide.models.Range (shared.jar), which was referenced from the common libs/ but never committed, so a clean checkout could not build. Ship the jar plugin-locally under pair/libs/ instead of the stage-tracked common libs/. Also drops the ndk abiFilters block: the plugin has no native code. Jar built from CodeOnTheGo :shared:jar. * ADFA-4395: Address plugin review — permissions, manifest, in-app help - Drop the unused native.code permission (no native code ships in the plugin) - Declare plugin.editor_tabs for the contributed Pair editor tab - Pin the supported IDE range: min_ide_version = max_ide_version = 26.32, the release that ships the peer-cursor/openProject plugin API - Implement DocumentationExtension: tooltip entry on the sidebar item (tag pair.sidebar) with summary, detail, and a User guide button, plus a Tier-3 offline guide at assets/docs/index.html * ADFA-4395: Fix tooltip category to full plugin.id, widen max_ide_version * ADFA-4395: Fix black button text in light mode — strip baked colors from M3 Typography Typography built from PluginTextStyles carried explicit colors (labelLarge had onSurface baked in), which override Material3 per-slot content colors — filled buttons rendered onSurface (black in light mode) instead of onPrimary. Typography is now colorless and PairContent's root Box is a Surface so LocalContentColor provides onSurface to plain text. Verified on emulator in light and dark mode. * ADFA-4395: Add long-press tooltips to every meaningful pair plugin control PairTooltips registers one entry per control (16 total) under the plugin category; the longPressTooltip modifier intercepts long-press in the initial pointer pass, shows the tooltip via IdeTooltipService with the 3-arg overload, and consumes the rest of the gesture so the underlying control does not also fire onClick. Verified on emulator: tooltips render with See more, taps and text input unaffected. * ADFA-4395: New Plugin Manager tile icon, code brackets with WiFi badge Rounded teal tile matching the other plugins' card style: big < > centered with a small WiFi fan at the top right. Day tile is deep teal with a white glyph, night tile is bright teal with a dark glyph. Sidebar and tab keep the existing people glyph. * ADFA-4395: Rename plugin to Code Together plugin.name, app label, and the user guide now say Code Together; the tab and sidebar keep the short Pair label and plugin.id is unchanged so existing installs update in place. Docs prose also drops its em dashes. * ADFA-4395: Align pair plugin with the standard toolchain (ADFA-4907) check-toolchain.sh flagged four drifts against Code On The Go's on-device toolchain: compileSdk/targetSdk 34 -> 36 and AGP 8.8.2 -> 8.11.0. AGP 8.11.0 needs Gradle >= 8.13, so drop pair's bundled 8.10.2 wrapper and use the repo-root 8.14.3 wrapper (the convention in CLAUDE.md; update-libs.sh already falls back to it when a plugin has no local gradlew). Verified assemblePlugin still produces pair.cgp with the bumped toolchain. --------- Co-authored-by: Daniel Alome <astrocoder007@gmail.com>
1 parent 8167fb0 commit 8e4df68

84 files changed

Lines changed: 7668 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ See the official [plugin documentation](https://www.appdevforall.org/codeonthego
3030
| [`vector-search-plugin/`](vector-search-plugin/) | Semantic code search using embeddings and vector similarity. |
3131
| [`get-ai-models/`](get-ai-models/) | Bottom-drawer catalog of curated, fully-open GGUF model files; downloads one to `/sdcard/Download` and verifies its SHA-256. |
3232
| [`project-to-template/`](project-to-template/) | Converts the open Android project into a Code On The Go (.cgt) template and installs it into the IDE's New Project picker. |
33+
| [`pair/`](pair/) | Real-time pair programming across two devices on the same WiFi — host or join a session and sync edits and cursor presence live over the local network. |
3334

3435
## Building a plugin
3536

‎pair/.gitignore‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.gradle/
2+
build/
3+
.idea/
4+
local.properties
5+
*.iml
6+
.DS_Store
7+
captures/
8+
.externalNativeBuild/
9+
.cxx/

‎pair/README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Pair
2+
3+
A plugin for [CodeOnTheGo](https://github.com/appdevforall/CodeOnTheGo) that turns the IDE into a real-time collaborative editor. Two phones on the same WiFi share one editing session: one device **hosts**, others **join** by typing the host's `ip:port` or scanning its QR code, and from then on edits, cursors, and file opens flow between devices as they happen — no server, no cloud, no signup.
4+
5+
It surfaces as a **Pair** tab in the editor. Host a session and an invite card shows the address and a QR code; join one and the peer list fills in. When the host types in `MainActivity.kt`, the guest sees the same file open and the text arrive keystroke by keystroke, with each peer's live position shown in the peer list.
6+
7+
## Building
8+
9+
```sh
10+
cd pair
11+
./gradlew clean assemblePlugin
12+
```
13+
14+
The `.cgp` lands in `build/plugin/`. Install it from inside CodeOnTheGo via the Plugin Manager. Always `clean` first — the plugin builder copies the built APK into the `.cgp` and then deletes the source APK, so an incremental build can package an empty artifact.
15+
16+
## How it works
17+
18+
- **Pair tab** — contributes an editor tab and sidebar entry via `EditorTabExtension` + `UIExtension`; the whole UI is Jetpack Compose (Home, Host, Guest, and QR-scan screens).
19+
- **Edit sync** — subscribes to the host IDE's `DocumentChangeEvent` on the EventBus for local edits and replays remote edits through `IdeEditorService.replaceRange(...)`. A single loopback guard stops an applied remote edit from rebroadcasting as a local one.
20+
- **File-relative identity** — a file's wire identity is its path **relative to the project root** (`IdeProjectService.getCurrentProject().rootDir`). The sender strips the root; the receiver re-anchors to its own, so a session works across two devices whose projects live at different absolute paths.
21+
- **Presence** — cursor positions ride alongside edits; each peer gets a color, shown in the peer list and (with the extended API below) as an inline caret inside the editor.
22+
- **Transport** — a star topology over `ws://`: the host runs a `WebSocketServer`, each guest opens one `WebSocketClient`, and the host echoes messages to the other guests. Messages are compact JSON (`hi` / `edit` / `cur` / `fo` / `fc` / `ff` / `sync` / `bye`).
23+
- **Conflict handling** — per-file sequence numbers with the host as authority; a divergence flags the session *out of sync* rather than dropping the edit, and the host can push an authoritative resync of the full file.
24+
- **Session history** — past sessions persist as JSON under `IdeEnvironmentService.getPluginDataDirectory()`; the Home screen lists them to rename, delete, or reconnect.
25+
26+
## Source layout
27+
28+
```
29+
pair/
30+
├── build.gradle.kts, settings.gradle.kts, proguard-rules.pro
31+
└── src/main/
32+
├── AndroidManifest.xml plugin id, main class, icons, permissions
33+
├── assets/ icon_day.png, icon_night.png (190×190 interlocking-rings mark)
34+
└── kotlin/com/appdevforall/pair/plugin/
35+
├── PairPlugin.kt IPlugin + EditorTabExtension + UIExtension entry
36+
├── data/ wire protocol, WebSocket server/client, session store
37+
├── domain/ EditBroker orchestrator, observer/applier, path mapping, peer registry
38+
├── ui/ Compose theme, components, and screens
39+
└── util/ LAN discovery, QR decode
40+
```
41+
42+
## Dependencies from `libs/`
43+
44+
- `../libs/plugin-api.jar` — the plugin API surface (`IPlugin`, extensions, `Ide*Service`); `compileOnly`, provided by the IDE at runtime.
45+
- `../libs/gradle-plugin.jar` — the `com.itsaky.androidide.plugins.build` Gradle plugin that packages the `.cgp`.
46+
- `../libs/eventbus-events.jar` — the editor and file `*Event` types Pair subscribes to on the EventBus.
47+
- `../libs/shared.jar` — `com.itsaky.androidide.models.Range`, the type of `DocumentChangeEvent.changeRange` that Pair reads.
48+
49+
Jetpack Compose is linked `compileOnly` (host-provided), not bundled.
50+
51+
> **Note:** Pair requires an extended `plugin-api` beyond the current `stage` baseline — `IdeProjectService.openProject(File)` (open a project after a pull-model sync) and `IdeEditorService.showPeerCursor` / `hidePeerCursor` / `clearPeerCursors` (inline remote-cursor decoration). These land on the `feat/ADFA-4419-remote-peer-editor-decoration` branch. If `assemblePlugin` fails with unresolved references to those symbols, the shared `libs/` jars are older than the API Pair needs; refresh them from a CodeOnTheGo build that includes the extensions (`../scripts/update-libs.sh --local <path-to-CodeOnTheGo> --ref feat/ADFA-4419-remote-peer-editor-decoration`).
52+
53+
## License
54+
55+
Pair is an open-source example plugin for Code on the Go. Its source is licensed per the surrounding `plugin-examples` repository (see `LICENSE` at the repo root). It makes no cloud calls — all traffic stays on the local network between the paired devices.

‎pair/build.gradle.kts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
plugins {
2+
id("com.android.application")
3+
id("org.jetbrains.kotlin.android")
4+
id("org.jetbrains.kotlin.plugin.compose")
5+
id("com.itsaky.androidide.plugins.build")
6+
}
7+
8+
pluginBuilder {
9+
pluginName = "pair"
10+
}
11+
12+
android {
13+
namespace = "com.appdevforall.pair.plugin"
14+
compileSdk = 36
15+
16+
defaultConfig {
17+
applicationId = "com.appdevforall.pair.plugin"
18+
minSdk = 26
19+
targetSdk = 36
20+
versionCode = 1
21+
versionName = "1.0.0"
22+
}
23+
24+
buildTypes {
25+
release {
26+
isMinifyEnabled = false
27+
isShrinkResources = false
28+
signingConfig = signingConfigs.getByName("debug")
29+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
30+
}
31+
}
32+
33+
compileOptions {
34+
sourceCompatibility = JavaVersion.VERSION_17
35+
targetCompatibility = JavaVersion.VERSION_17
36+
}
37+
38+
buildFeatures {
39+
compose = true
40+
}
41+
42+
packaging {
43+
resources {
44+
excludes += setOf(
45+
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
46+
"META-INF/DEPENDENCIES",
47+
"META-INF/LICENSE",
48+
"META-INF/LICENSE.txt",
49+
"META-INF/NOTICE",
50+
"META-INF/NOTICE.txt",
51+
"META-INF/INDEX.LIST",
52+
"META-INF/io.netty.versions.properties"
53+
)
54+
}
55+
}
56+
}
57+
58+
dependencies {
59+
compileOnly(files("../libs/plugin-api.jar"))
60+
compileOnly(files("../libs/eventbus-events.jar"))
61+
compileOnly(files("libs/shared.jar"))
62+
63+
implementation("com.google.android.material:material:1.10.0")
64+
implementation("androidx.fragment:fragment-ktx:1.8.8")
65+
implementation("androidx.core:core-ktx:1.12.0")
66+
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
67+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
68+
implementation("org.jetbrains.kotlin:kotlin-stdlib:2.3.0")
69+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
70+
71+
implementation("org.java-websocket:Java-WebSocket:1.5.6")
72+
implementation("org.slf4j:slf4j-nop:1.7.36")
73+
74+
compileOnly("org.greenrobot:eventbus:3.3.1")
75+
76+
val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
77+
compileOnly(composeBom)
78+
compileOnly("androidx.compose.ui:ui")
79+
compileOnly("androidx.compose.ui:ui-graphics")
80+
compileOnly("androidx.compose.ui:ui-tooling-preview")
81+
compileOnly("androidx.compose.foundation:foundation")
82+
compileOnly("androidx.compose.material3:material3")
83+
compileOnly("androidx.compose.runtime:runtime")
84+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
85+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
86+
87+
debugImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
88+
debugImplementation("androidx.compose.ui:ui-tooling")
89+
debugImplementation("androidx.compose.ui:ui-tooling-preview")
90+
debugImplementation("androidx.compose.ui:ui")
91+
debugImplementation("androidx.compose.ui:ui-graphics")
92+
debugImplementation("androidx.compose.foundation:foundation")
93+
debugImplementation("androidx.compose.material3:material3")
94+
debugImplementation("androidx.compose.runtime:runtime")
95+
}
96+
97+
kotlin {
98+
compilerOptions {
99+
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
100+
}
101+
}

‎pair/gradle.properties‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
2+
org.gradle.parallel=true
3+
org.gradle.caching=true
4+
android.useAndroidX=true
5+
android.nonTransitiveRClass=true
6+
kotlin.code.style=official

‎pair/libs/shared.jar‎

106 KB
Binary file not shown.

0 commit comments

Comments
 (0)