Skip to content

Commit 67a96c3

Browse files
janrick123claude
andauthored
J Code: native Android IDE — editor, terminals, Build & Run, and an extension marketplace (#1)
* initial stuffs * Add verification plan, mock UI, and update project structure - Created a new verification plan document for J Code, detailing phase checklists for feature validation. - Added a mock UI image to the plans directory. - Updated settings.gradle.kts to include new core and feature modules: resource and marketplace. - Added native modules for core and proot to the project structure. - Created a new wd.xml file to handle missing directory errors. * Fix native-handle leaks, terminal lifecycle, and the build/run pipeline Stability: - Buffer/Snapshot Cleaners captured `this`, permanently defeating the native-handle safety net (leaked a native snapshot every keystroke). Capture only the Long handle, via static nativeCloseByHandle in jni_buffer.cpp, so they actually fire; wrap transient snapshot reads in .use{}. - PtyProcess/VtParser: capture-free Cleaners; remove VtParser's banned finalizer (+ static nativeCloseByHandle in jni_vt.c). - pty.cpp: return -1 on EOF (was 0) so a finished shell's reader breaks instead of spinning forever; TerminalSessionManager auto-reaps the exited session, releases the foreground-service hold, and the UI drops the tab. Make the session map thread-safe across the IO reader. - TerminalView: reuse a char[] in onDraw (no per-cell String alloc) and coalesce repaints to one per frame; guard draws against a closed parser. - Fix a forward-referenced local fun that broke the build. Build/run: - ProjectRunner: templateId-first detection (no longer reports "no run configuration" for a scaffolded project) with self-healing recipes that scaffold/build a missing client live in the run terminal. - Publish the ASP.NET server to ext4 and launch the managed DLL via the dotnet host -- /workspace is a noexec FUSE mount, so the apphost cannot be exec'd there. Version-agnostic target framework (newest installed SDK). Verified on device (AYN Odin2): ASP.NET Core + React/Vite scaffold->build ->run renders in the browser at localhost:5080. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Hide non-functional destinations and trim over-described UI - Hide the Search and SCM activity-bar destinations (no working UI yet) and the disabled Problems / Debug Console right-panel tabs, via available/enabled flags filtered at every render site (kept in the enums for `when` exhaustiveness). A stale persisted tool selection now falls back to Explorer. - Drop the Output panel's needless "no PII" line; collapse the now-dead disabled-tab color branches (also removes a sub-spec contrast value). Verified on device: rail shows Explorer/Run/Extensions/SDK/Settings; the right panel shows only Terminal/Output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add a Stop control to the Run panel Running a project left the server (dotnet/vite) running in its terminal with no obvious way to stop it from the UI. The Run panel now shows Stop + Re-run side by side while a run is active (building, or server up). Stop (handleStopRun) sends Ctrl-C to the run terminal for a graceful shutdown, cancels the server-ready poll, and resets run state; the run terminal is kept so its output stays visible and Re-run can reuse it. A run session that exits on its own now also clears run state. Verified on device: Stop/Re-run appear during a run and Stop returns the panel to Build & Run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Disable npm progress bar in run recipes (was garbling the terminal) npm's animated progress gauge (light-shade chars redrawn via carriage returns) accumulated into a wall of dotted garbage in the run terminal during `npm install`. Export npm_config_progress=false (plus fund/audit) at the top of both run recipes so npm install/create emit clean, line-by-line output. Verified on device: the [3/5] npm install step now streams cleanly with no progress-bar garbage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Run recipes from a script file instead of an inline heredoc The run recipe was fed into the interactive shell as a `bash <<'JCRUN'` heredoc, so bash echoed every line back with its `>` continuation prompt — dumping the whole ~30-line recipe into the run terminal before any output appeared. Write the recipe to <project>/.jcode/run.sh and invoke it with a single `bash <path>` line instead (the script is read, not executed, so the noexec /workspace mount is fine). The terminal now shows just the clean [1/5]..[5/5] step echoes. Falls back to the inline heredoc if the script cannot be written. Pairs with the earlier npm_config_progress=false fix; together the run terminal renders cleanly. Verified on device: no recipe dump, no progress- bar garbage, build/run still serves at localhost:5080. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix editor font rendering ~2.3x too small (sp treated as px) The editor Renderer set Paint.textSize directly from RenderConfig.fontSizeSp (an sp value), but Paint.textSize is in pixels — so a 14sp font rendered at 14px, i.e. density times too small (about 2.3x on the 369dpi Odin2). Multiply fontSizeSp by the display density (passed into Renderer) when computing the glyph size, line-number size and line height. Apply the same conversion to EditorView's tap hit-test and gutter-width math so taps still map to the correct caret. Build: green. Device visual verification pending (wireless ADB dropped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Render terminal progress bars in place (handle CSI G / CHA) The VT parser didn't implement CSI 'G' (Cursor Horizontal Absolute) — the escape Node's readline.cursorTo(0) emits to return to column 0 before redrawing a progress line in place. Without it the cursor never reset, so npm's progress (and any CHA-based spinner/bar) marched down the screen into a wall of dotted garbage. Add CHA ('G') and VPA ('d') handling so progress redraws overwrite a single line; this fixes progress rendering for every CLI (npm, git, agent tools), not just the run recipe. Re-enable npm's progress bar in the run recipes now that it renders correctly (fund/audit stay off). Verified on device: npm install shows a single in-place spinner/progress line during Build & Run instead of garbage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Run projects in dev: separate Server + Client terminals Build & Run now launches a development setup instead of a production build-and-serve. For ASP.NET Core + Vite React it spawns two terminals, started server-first: - "Server": dotnet build -c Debug to ext4, then run the DLL with ASPNETCORE_ENVIRONMENT=Development on :5080. - "Client": Vite dev server (npm run dev) on :5173. The browser opens the dev frontend (:5173). RunPlan now carries a list of labelled RunTerminals; handleRun tears down the previous run's terminals, spawns one per step, stops them together (Stop), and reaps them via the exit listener. createSession takes a custom label, and the terminal tab bar shows it (Server/Client) instead of "bash N". Known limitation: the client runs from an ext4 staging copy (the FUSE /workspace can't host node_modules), so HMR is on that copy, not live /workspace edits - a follow-up can bind node_modules for true live HMR. Verified on device: Server + Client tabs spawn server-first and the browser opens the Vite dev server at localhost:5173. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix "multiple DataStores active" crash (UI-preferences singleton) MainViewModel created the ui-preferences DataStore with PreferenceDataStoreFactory.create per instance, and that factory's scope is never cancelled. So when a second MainViewModel is constructed (e.g. after the Activity is recreated returning from the browser on Build & Run), a second DataStore for the same file is created and crashes with "There are multiple DataStores active for the same file". Hold the store in a process-singleton (UiPreferencesStore) created exactly once per process. Verified on device: the action that reliably crashed before no longer does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add long-press action menus to terminals Content (long-press the terminal output) -> popup: Select text (arms a drag-selection for the next touch), Select all (selects the visible screen and copies), Paste (clipboard -> PTY, the first touch way to paste), Clear (Ctrl-L). Replaces the old immediate long-press-drag selection. Tab (long-press a session tab) -> dropdown: Rename, Clear (Ctrl-L to that PTY), Close, Close others, Close all. Rename uses a dialog + an observable label override (Session.label is now mutable) so the tab updates live. Verified on device: both menus open with the right items. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Terminal: double-tap to type, single tap opens links/paths (configurable) A single tap no longer raises the keyboard (it was too easy to trigger by accident). New default behavior: - Single tap: opens the token under the finger - a URL in the browser, or a file path in the editor (resolves /workspace, absolute host, and project-relative paths; strips a trailing :line). Non-link taps do nothing. - Double tap: focuses and shows the keyboard, force-shown so it returns even after a Back dismiss. Opening/switching a terminal now focuses without auto-raising the keyboard. - Long-press still opens the action menu; "Select text" still drag-selects. Configurable in App Settings > Terminal > "Double-tap to type" (app-wide, default on; off restores single-tap-to-type), stored in the ui-preferences DataStore. The tap config reaches the deeply-nested terminal view via a CompositionLocal to avoid prop-drilling through the sidebar layers. Verified on device: single tap doesn't raise the keyboard; double tap does; tapping an echoed URL opened the browser; the settings toggle is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Rename app "Preferences" -> "App Settings" Renames the app-level settings destination and its page tab from "Settings"/"Prefs" to "App Settings", so the name reads clearly against project-specific "Project Settings". Updates the rail tooltip/a11y label, the compact header chip, the settings page tab title, and a stale comment. Verified on device: the destination opens an "App Settings" tab and the breadcrumb reads ".../App Settings". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Don't auto-show the right drawer on launch The right drawer's initial visibility seeded from `isLandscape`, so on a landscape device it auto-opened (overlaying the editor) on every cold launch. Seed it `false` instead, matching portrait, so launch never auto-opens the drawer. The rememberSaveable keys are kept: a drawer the user opens still survives Activity recreation, and rotation still re-seeds per the existing design. All open paths are unchanged - Run, the terminal/Show-Terminal toggle, and the sidebar toggle still open it on demand. Background terminal sessions are decoupled from drawer visibility (a session still auto-starts and simply renders when the drawer is opened). Verified on device: cold launch shows a full-width editor with no drawer; the terminal toggle opens it and the prior session is restored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * SDK Manager: compact layout The SDK Manager is dense (13 entries x 3 buttons), so the default Material3 button size and ~16sp titles dominated the narrow side panel. Tighten it: - Card/notice titles -> titleSmall; entry names + summary values -> bodyMedium. - Shared CompactFilledButton/CompactOutlinedButton helpers (32dp min height, 12dp/4dp content padding, labelMedium text) replace the six default-sized buttons (Refresh/Setup, Install/Verify/Remove, Show). - Trim padding (12->10) and inter-element spacing across cards and rows. - Shorten the verbose card/category/output descriptions to one line each. Verified on device: titles, entry rows, and action buttons render compact and legible in the side panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add configurable theme bundles + swappable icon bundles Foundation for a consistent, customizable look, selectable app-wide. Design tokens (core/design): - ThemeBundle: a named look = Material dark+light color schemes + matching semantic colors (success/warning/info, which Material's scheme lacks) + an optional font. Five built-ins: Catppuccin (default, the prior palette), Dracula, Nord, Gruvbox, One Dark. Registry resolves by id, falls back to default. The data shape is what a disk/asset bundle would deserialize into. - JCodeSemanticColors + JCodeSpacing tokens, exposed via LocalSemanticColors / LocalSpacing and a JCodeTheme accessor. - IconBundle: JCodeIcon semantic slots resolved through the active bundle, with per-slot overrides falling back to the built-in default, so a custom pack can ship just its hero icons. Default "Material Rounded" bundle unifies icons to the Rounded family. All ~30 call sites (JCodeShell, ExplorerView, RunDebugPanel) now reference JCodeIcon via jcIcon() instead of Icons.* — the whole icon set is swappable from one place. Wiring: - M3Theme takes a ThemeBundle + IconBundle and provides the locals. - App-wide selection persisted in the ui-preferences DataStore (theme_bundle_id / icon_bundle_id); resolved in MainActivity into M3Theme. - App Settings gains "Theme bundle" (palette swatches) and "Icon bundle" (icon preview) pickers. Verified on device: switching theme bundle recolors the whole app live and persists; icon refactor renders identically under the default bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add "J Code Line" custom vector icon bundle A hand-authored minimal line icon set (~22 ImageVectors on a 24x24 grid: folder, run, stop, terminal, search, settings-as-sliders, extensions-as-blocks, sdk-as-cube, scm-branch, code, chevrons, etc.). Registered as a second icon bundle; overrides its hero slots and falls back to the Material default for the long tail. Selectable from App Settings > Icon bundle. Init order matters: the icon map is declared before the bundle/list so top-level initialization populates it first (an earlier ordering passed a null overrides map and crashed at startup). Verified on device: selecting it swaps the whole app's chrome to the custom line icons live and persists across launches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Browse/apply bundles in the Ext tab; use semantic success color Extensions tab: below the project templates, add "Theme bundles" and "Icon bundles" galleries (palette swatches / icon previews) with tap-to-apply and an "Active" marker, mirroring the App Settings pickers. Threaded the selection state + callbacks through WorkspacePanel -> ExtensionsPanel. Consistency: Onboarding's log lines used a hardcoded green (Color(0xFF4CAF50)) that ignored the theme; replaced with JCodeTheme.semanticColors.success so it tracks the active bundle. Verified on device: the Ext galleries list all bundles, mark the active one, and apply on tap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Install marketplace extensions at runtime; externalize templates Templates are no longer bundled with the app. The marketplace and its extensions now live in their own repos; the app fetches the index and installs extensions on-device at runtime. - ExtensionInstaller: fetches the marketplace index (marketplace.yaml) over HTTPS, installs an extension by downloading its GitHub repo zip (codeload) and unpacking it under filesDir/extensions/<id>/, and scans/uninstalls installed extensions. No new deps (HttpURLConnection + ZipInputStream). Parses templates and the new `language` pack manifest (completions / formatter / helpers). - TemplateCatalog now aggregates templates from installed `templates` extensions instead of bundled assets; removed the assets srcDir and the old asset loader and deleted the in-repo marketplace/ directory (moved to j-code-ext-template-1). - MainViewModel exposes installedExtensions / marketplaceEntries / busy + refresh, install, and uninstall. - Ext tab: a Marketplace section (Install) and an Installed section (Remove), above the theme/icon bundle galleries. Verified on device: the Ext tab fetched the live index (4 extensions); installing "JCode Project Templates" and "C# Language Pack" downloaded + registered them, and the installed templates populate the New-project dialog. Editor execution of language packs (live completions/formatter) is a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Pack-driven syntax highlighting + editor render fixes Language packs now carry a syntax definition (keywords/types/strings/comment); the editor colors the active file when a matching pack is installed. - LanguagePack gains syntax + formatting fields; ExtensionInstaller parses them. - SyntaxHighlighter: a small, dependency-free tokenizer producing ColoredSpans (UTF-8 byte offsets) for comments/strings/numbers/keywords/types from the pack rules. Wired in JCodeShell: for the active file, resolve the installed pack by extension, tokenize on each edit (off-thread), and push a GLYPH_COLOR layer. Editor render fixes (the coloring pipeline existed but was never driven): - EditorView.onDraw now passes decorations to Renderer.draw (was defaulting to EMPTY, so ColoredSpans never drew), and attach() observes the decorations and caret flows to invalidate on change. - Viewport.lineHeightPx (default 20, never updated) is now synced from the render config, and a newly-attached EditorState seeds its viewport from the current view size. Without these, only ~2 lines were considered visible on tab switch. Verified on device: installing the TypeScript pack colors a .ts file (keywords purple, types cyan, strings green, comments gray, numbers orange) and the full file renders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Editor long-press context menu with pack-gated language actions Long-pressing in the editor selects the word under the finger and opens a context menu. Always: Copy / Cut / Paste / Select all (working, via the clipboard + EditTx). When the active file matches an installed language pack, the menu is extended with Go to Definition, Find References, Rename Symbol, and Format Selection. - EditorView: GestureDetector long-press -> select word (offsetAt + wordAt) -> onContextRequest(x, y, word); clipboard/selection helpers (copy/cut/paste/ selectAll); EditorContextRequest + EditorLanguageAction types. - EditorViewHost: anchored DropdownMenu; clipboard items call the view, language items (shown when languageActionsEnabled) call onLanguageAction. - JCodeShell: enables language actions when the active file matches an installed pack; semantic actions are deferred to a language server and currently show a "coming soon" message (per the chosen approach). Verified on device: long-press a .ts identifier shows the extended menu; a .yaml file (no pack) shows only clipboard actions; tapping Go to Definition surfaces the deferred-LSP message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extension versioning: show updates + Update action The marketplace index now carries each extension's latest version; the app compares it against the installed copy's version. - MarketplaceEntry gains version; ExtensionInstaller parses it; compareVersions / isUpdateAvailable helpers (dotted, numeric). - Ext tab: each row shows its version; an installed-but-outdated extension shows "Update" (re-downloads the latest) instead of "Installed", and the Installed section shows "vX -> vY". Up-to-date shows "Installed"; missing shows "Install". Verified on device: bumping the C# pack to v0.2.0 (repo + index) surfaced an Update on the v0.1.0 install; tapping Update pulled v0.2.0 and the row flipped to Installed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Settings: search filter + section grouping App Settings now opens with a "Search settings" field and groups the cards under section headers (Overview / Appearance / Environment / Editor / Terminal / Files). - LocalSettingsQuery composition local holds the query; SettingsCard self-hides when the query matches neither its title, description, nor optional keywords. - SettingsSectionHeader renders the group label, and hides while searching so results read as a flat filtered list. Verified on device: headers show when idle; typing "terminal" narrows to the Terminal card only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extension taxonomy + dependency suggestions on install Extensions can now declare category/subcategory and require/suggest other extensions, SDKs, and LSPs; the marketplace index carries them. - MarketplaceEntry gains category/subcategory + requires/suggests (ExtensionDeps: sdks/lsps/extensions); ExtensionInstaller parses them. ExtensionType adds Formatter (for future formatter extensions). - Ext tab rows show "type · Category/Subcategory · vX". - Installing an extension with deps opens a non-blocking dialog with Required and Suggested groups: other-extension deps install inline (or show "Installed"); SDK/LSP deps are listed (SDK Manager / language-server). "Install <name>" proceeds regardless. - marketplace.yaml: template pack suggests the C#/TS/JS packs (+ dotnet/nodejs SDKs); language packs require their SDK and suggest their language server. Verified on device: rows show categories; installing JCode Project Templates shows a Suggested dialog (C#/TypeScript = Installed, JavaScript = Install). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Settings: Formatter selection (built-in + future formatter extensions) Adds an app-wide Formatter choice (ui-preferences "formatter_id", default "builtin"). App Settings > Editor shows a "Formatter" card listing Built-in plus any installed type:formatter extensions, so installing a formatter extension later adds it to the list automatically. Verified on device: the Formatter card shows Built-in selected and is findable via the settings search ("format"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Refactor: extract workbench dialogs out of JCodeShell Move the New/OpenFolderType/Scaffold dialogs (and their TypeOption/TemplateOption helpers) from the 3449-line JCodeShell.kt into dev.jcode.workbench.dialog. WorkbenchDialogs.kt. Pure code movement, no behavior change; the two dialogs called from JCodeApp are internal, the rest file-private. JCodeShell -> 3151 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Refactor: extract Extensions/marketplace UI out of JCodeShell Move ExtensionsPanel + the dependency dialog + bundle gallery rows into dev.jcode.workbench.marketplace.ExtensionsPanel.kt (ExtensionsPanel internal, the rest file-private). Pure code movement. JCodeShell -> 2840 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Refactor: extract workbench model/enums out of JCodeShell Move TerminalTapConfig, LocalTerminalTapConfig, WorkbenchTool, RightPanelTab into dev.jcode.workbench.WorkbenchModel.kt (internal). Shared types now live in one place for the rest of the JCodeShell split to import. JCodeShell -> 2808 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Rework Environment setup: dialog -> in-editor page + readable distro list The cramped Environment setup dialog squished long distro labels into unreadable vertical character columns. Convert it to a full-width in-editor page tab and replace the segmented-button distro picker with a vertical RadioButton list (fixes both the page and the first-run screen, which share StepperScreen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add LSP Manager: install/verify/remove language servers per distro A new "LSP Manager" rail tool mirrors the SDK Manager: it installs, verifies, and removes language servers inside the active distro, tracks installed state per distro (DataStore), and streams command output. - core:distro is the single source of truth (LspServerCatalog) for the 7 built-in servers (clangd, typescript-language-server, csharp-ls, pyright, gopls, rust-analyzer, kotlin-language-server) including a new C# server. The install/verify/uninstall machinery lives in DistroService, reusing its mutex + execInDistro + DataStore. - core:lsp's LspServerDescriptor.BUILT_IN now derives from that catalog (core:lsp -> core:distro) so the runtime client and the manager never drift. Avoids the distro -> lsp -> term -> distro cycle. - feature:lsp-manager renders the compact panel; wired through MainViewModel + JCodeShell + a new JCodeIcon.Lsp. Verified on device: panel lists all 7 servers grouped by category with per-distro install state; Verify runs the real command in the distro (exit code + stderr captured, status + rolling log updated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Redesign SDK & LSP managers: compact rows + detail pages + status checks The drawer panels are now dense, tappable rows (name, one-line description, status chip) instead of inline buttons. Tapping a row opens a full in-editor detail page (title, description, Install/Update/ Uninstall + Verify + rolling output). - Status is checked live when a manager comes into view: a full installed + update-available check runs async in the background (guarded against re-entry), surfacing a "Checking…" state and flipping rows to Installed / Update available as results arrive. - "Update available" is detected per entry via an optional updateCheck (apt list --upgradable for apt packages; npm outdated for npm globals); entries without a reliable check just show Installed. - Shared manager UI (rows, status chip, section card, detail screen) extracted to core/design ManagerUi.kt and used by both managers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Redesign Extension Manager: compact rows + detail page with samples The Extensions panel now mirrors the SDK/LSP managers: dense rows (name, one-line description, status chip: Installed / Update available / Not installed) that open a full in-editor detail page. The detail page shows title, description, a samples section, a requirements section (required/suggested SDKs, LSPs, and extensions with install state), and Install / Update / Uninstall actions (the dependency prompt moved here). Theme and icon bundle galleries stay in the panel. - MarketplaceEntry/InstalledExtension gain description, longDescription, and samples (List<CodeSample>); the index + manifest parsers read them. - The shared ManagerDetailScreen gains showVerify/showOutput flags (extensions hide both) and is now vertically scrollable so tall detail pages reach their actions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Rename Settings, slim SDK/LSP panels, system-back nav, fix verifier limit - Rename "App Settings" -> "Settings" (rail tool + page tab); the Global and Project/Workspace scopes already live on that screen. - Remove the environment-config block (distro/status + "Environment setup") from the SDK and LSP manager panels; environment config stays on the Settings screen, which already hosts it. Panels keep just the installed count + Refresh, and point to Settings when setup is needed. - Detail pages are one-per-type now: opening an SDK/LSP/Extension detail reuses a single tab of that kind instead of stacking new tabs. - System back navigates back a step (close the active page/detail tab, then modal drawer, then right sidebar). With nothing left, a second back within 2s exits — or, if a run is in progress or a terminal is live, backgrounds the app so the work keeps running. - Fix a launch-time VerifyError: JCodeShell had so many parameters that ART's verifier rejected the method (register overflow). Bundle the 14 SDK/LSP/Extension manager callbacks into WorkbenchManagerActions to cut the param count, and extract the back-handling into WorkbenchBackHandler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Build & Run: per-project config in .jcode/run.yaml + multi-project panel The Run panel now lists projects instead of just the open one: every project in a User Workspace gets a row, while the Default Workspace shows the single open project. Each row shows the run kind + a serve URL and offers Build & Run and Configure. - New per-project config at /{project}/.jcode/run.yaml (RunConfig: name, readyPort, terminals[label+command]), read/written by RunConfigStore (core:config). Optional run scripts already live at .jcode/run-<label>.sh. - ProjectRunner.effectivePlan prefers a saved run.yaml, else falls back to template/filesystem detection. editableRunConfig seeds the editor from the detected plan (or a blank starter for unrecognized projects). - Configure opens a structured in-editor page (EditorPageKind.RunConfig): name, ready port (blank = no browser), and an add/remove list of terminals with editable bash commands; Save writes run.yaml. - handleRun is now per-project; readyPort 0 runs terminals without opening a browser. The top-bar run button runs the selected project. Verified on device: Default Workspace shows the open project; Configure pre-fills from the detected plan; Save writes .jcode/run.yaml; the panel then reads the config back. No verifier crash (manager callbacks stay bundled; +3 run params are well under the limit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Redesign Build & Run panel: action-first compact rows Replace the chunky per-project card (two big buttons, a wrapping label) with a dense single row per project: name + a status chip (Idle / Building… / Running / Not set up) on top, "<kind> · :<port>" beneath, and inline controls — Run (▶) when idle, Stop (■) + Open-in-browser (↗) while running, plus a Configure (gear) action. One tap to run, far less vertical space, consistent with the redesigned SDK/LSP/Extension panels. Verified on device: Idle row -> tap Run -> row shows Building… + Stop + Open-in-browser -> tap Stop -> back to Idle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Fix editor: working soft keyboard + correct text input The editor was unusable: the soft keyboard never opened, and once it did (after wiring onCheckIsTextEditor + showSoftInput) every keystroke corrupted the buffer. Root cause was twofold: - The native piece-tree (native/buffer/piece_tree.cpp) is an unfinished skeleton — its insert/split orphans the inserted text and leaves the original piece in place, duplicating the whole document on the first edit; red-black balancing and remove are empty stubs. It had never been exercised on-device because input never worked. Fall back to the correct pure-Kotlin array-splice buffer via USE_NATIVE_BUFFER=false until the tree is fixed. - The IME path desynced: initialSel was hardcoded to 0, the caret was never reported back, and composing edits ran against Gboard's stale cursor model. Report the real caret in onCreateInputConnection, restartInput on tap, updateSelection after every edit, and disable composing/autocorrect (VISIBLE_PASSWORD) so each keystroke commits directly — the right model for a code editor. Anchor commit/compose to the composing-region start and advance the caret per newCursorPosition. - Editing keys arrive as key events, not commitText, so handle Backspace, Forward-delete, Enter, Tab, arrows, and hardware-keyboard characters in dispatchKeyEvent (UTF-8-aware caret stepping). Verified on device (Odin2): type, backspace, Enter, and tap-to-reposition all produce a correct buffer with correct rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add configurable symbol bar + hide-suggestions toggle for the editor The IME's suggestion/autocorrect strip is useless for code, and a touch keyboard makes brackets/punctuation slow to reach. Add two independent, on-by-default editor settings plus a quick-insert symbol bar. - :core:design SymbolBar.kt: a horizontally-scrollable bar of code keys (Tab + configurable symbols + caret arrows) that sits above the keyboard. Keys use detectTapGestures (not clickable) so a tap never steals focus and dismisses the keyboard. Also EditorKeyboardSettings — a single holder for the two flags + key list + onChange, so JCodeShell gains only one parameter (it sits near the ART verifier's register limit; same mitigation as WorkbenchManagerActions). - :core:editor EditorView: suppressSuggestions now drives the IME input type (VISIBLE_PASSWORD/NO_SUGGESTIONS when on, plain multi-line text when off) and restarts input when flipped while focused. Exposes insertTextAtCaret / insertIndent / moveCaretBy so the symbol bar can drive the active editor. - :app MainViewModel: three DataStore prefs (editor_hide_suggestions, editor_show_symbol_bar default true; editor_symbol_keys newline-delimited). - :feature:settings: a "Soft keyboard" card with the two toggles and a chip editor (add / remove / reset) for the symbol keys. - :feature:editor-pane: renders the bar at the bottom of the pane when a file editor is open with the keyboard up, wired to the active EditorView. Verified on device (Odin2): bar shows/hides per toggle, inserts symbols at the caret without dismissing the keyboard, Tab/arrows work, hide-suggestions flips the IME strip, and the key list persists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Replace soft-keyboard options with a full in-app keyboard There's no reliable way to suppress Gboard's toolbar/suggestion strip from an app, so instead own the keyboard entirely. A single "Use in-app keyboard" setting (on by default) replaces the previous hide-suggestions / show-symbol-bar toggles. - :core:design InAppKeyboard.kt: a self-contained on-screen code keyboard — QWERTY with one-shot Shift, a ?123 numbers/symbols layer, a configurable code-symbol row (Tab + symbols + ←↓↑→ + a ⌄ hide key), and a bottom row (?123/ABC, comma, space, period, Enter). Backspace fires once on press and auto-repeats while held. Keys use pointer gestures (never clickable) so taps don't steal focus from the editor. Layout is data-light so extensions can supply fuller custom layouts later. EditorKeyboardSettings is the single host param (mirrors the WorkbenchManagerActions verifier mitigation). - :core:editor EditorView: useInAppKeyboard suppresses the system IME (onCheckIsTextEditor returns false, taps request the in-app keyboard instead of showSoftInput). Input arrives through public insertTextAtCaret / insertIndent / backspace / moveCaretBy / moveCaretLineBy. The system-keyboard path keeps the VISIBLE_PASSWORD/no-compose input type for when the feature is turned off. - :app MainViewModel: editor_use_inapp_keyboard (default true) + editor_code_keys (configurable code row) DataStore prefs. - :feature:settings: one "Use in-app keyboard" toggle + the code-symbol-row chip editor (add / remove / reset). - :feature:editor-pane: hosts InAppKeyboard at the bottom of the pane, tracks its own visibility (no IME insets to key off), and maps key actions to the active EditorView. Removes the old SymbolBar. Verified on device (Odin2): default-on shows the in-app keyboard (no Gboard); letters, code symbols, Shift, ?123, Tab, Space, Enter, arrows, single + hold-repeat Backspace, and Hide all work and keep the buffer correct; toggling the setting off restores the system keyboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Make the in-app keyboard an app-wide bottom surface The keyboard was nested inside the editor pane, so it was only as wide as that pane (the left rail sat beside it) and tied to the editor's layout. Lift it to the app root as a single full-width surface driven by a shared controller. - :core:design: add InAppKeyboardController (visible + a KeyActionSink) exposed via LocalInAppKeyboard. An input target registers its sink and calls show() on focus; the controller's dispatchKey() routes keys to it (and handles Hide). Editor today; terminals/fields can plug into the same controller later. - :app JCodeShell: render one InAppKeyboard at the root, pinned full-width to the bottom, shown when the setting is on and the controller is visible. Its measured height pads the content above so the caret stays visible (content resizes; the keyboard isn't a floating overlay). Provided via LocalInAppKeyboard. - :feature:editor-pane: EditorViewHost now registers a KeyActionSink (mapping KeyAction -> the active EditorView) and calls controller.show() on tap / hide() on release; the per-pane keyboard rendering + lifted view state are gone. Verified on device (Odin2): tapping the editor raises a full-width keyboard at the app bottom; the editor + status bar resize above it; typing, backspace and the ⌄ hide key all work; no VerifyError. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Hide the in-app keyboard when a physical text keyboard is attached When a hardware keyboard is connected there's no reason to show the on-screen keyboard — hardware keys already reach the editor via EditorView.dispatchKeyEvent. JCodeWindowInfo.hasPhysicalKeyboard is too loose for this: it counts any non-virtual device with keys, which includes game controllers (the Odin2's built-in Odin Controller reports KEYBOARD_TYPE_NON_ALPHABETIC), so gating on it would hide the keyboard with no real keyboard attached. Add a stricter hasTextKeyboard (configuration.keyboard == QWERTY and not hidden, or a non-virtual input device with KEYBOARD_TYPE_ALPHABETIC) and gate the app-wide in-app keyboard on !windowInfo.hasTextKeyboard. It updates reactively via the existing InputManager.InputDeviceListener hot-plug path. Verified on device (Odin2): with only the built-in controller, hasTextKeyboard is false and the in-app keyboard still shows (no game-controller false-positive). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Remove the in-app keyboard and its settings The in-app keyboard didn't reach a good enough bar; using a dedicated code IME (e.g. Hacker's Keyboard) is the better path. Remove it entirely and let the editor use the system keyboard. - Delete core/design/InAppKeyboard.kt (keyboard, controller, sink, CompositionLocal, EditorKeyboardSettings, KeyAction). - EditorView: drop useInAppKeyboard / onInAppKeyboardRequested and the public insert/caret methods that only the in-app keyboard used; onCheckIsTextEditor is true again and a tap always raises the system IME. Keep the VISIBLE_PASSWORD / no-suggestions / no-compose input type so any system IME (incl. Hacker's Keyboard) commits each keystroke directly without corrupting the buffer. - JCodeShell: remove the root-level keyboard surface, the InAppKeyboardController, the content padding, and the editorKeyboard threading; the root is a plain imePadding Box again. - MainViewModel: drop the editor_use_inapp_keyboard / editor_code_keys prefs. - feature/settings: remove the "Soft keyboard" card + symbol-row editor. - feature/editor-pane: EditorViewHost no longer wires a key sink. - core/adaptive: drop the now-unused hasTextKeyboard from JCodeWindowInfo. Verified on device (Odin2): tapping the editor raises the system keyboard and typing edits the buffer correctly; no in-app keyboard, no Soft keyboard settings, no VerifyError. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Remove the minified left rail The icon-only left rail (DockRail) duplicated the tool switcher that already lives in the left sidebar header (WorkspaceHeader's SidebarToolButton row), so it was redundant. Remove it; the editor now uses the full width and tools are reached via the ≡ button → the sidebar's Files/Run/Ext/SDK/LSP switcher. - JCodeShell: drop the DockRail render, the DockRail + RailButton composables, orderedRailTools, the railTools/showPersistentRail locals, and the railToolOrder/onReorderRail params; prune the now-unused tooltip + semantics imports. - MainViewModel: drop the rail_tool_order preference and setRailToolOrder (only the rail used it; the sidebar switcher uses the fixed WorkbenchTool order). Verified on device (Odin2): no left rail, editor spans full width, the ≡ button opens the sidebar with its tool switcher + file explorer; no VerifyError. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add editor Save: Ctrl+S, top-bar button, dirty tracking, write-through Persist the active editor tab's buffer to its file via the Fs API. - EditorState: dirty StateFlow (set on applyEdit) + markClean() - EditorView: onSaveRequest + Ctrl+S in dispatchKeyEvent - EditorPane: thread onSave through to the EditorView - MainViewModel: saveActiveTab/saveTab write snapshot bytes via LocalFs; clear dirty only if the snapshot is unchanged; reflect EditorState.dirty onto EditorTab.isDirty for the tab dot - JCodeShell: Save button in the editor top bar (highlighted when dirty) - IconBundle: Save icon slot Device-verified on Odin2: edit -> dirty dot -> save -> persists across reopen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Tighten Settings typography + compact search field Card titles and several row labels (Toggle/Stepper/Option/Summary/Warning) used the default bodyLarge (16sp), inconsistent with the 14sp scale used elsewhere (section headers, bundle rows). Pin them all to bodyMedium so the screen reads as one consistent scale: section header (primary 14sp) > card title (14sp semibold) > row label/value (14sp) > description/supporting (12sp). Replace the tall default OutlinedTextField search box with a compact SettingsSearchField: a ~40dp rounded surface with a leading search icon, bodySmall placeholder/input via BasicTextField, and an × clear button when non-empty. Verified on device (Odin2): search is smaller, typing filters cards (e.g. "theme" → Theme bundle only), × clears it; card titles and values are no longer oversized. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Trim built-in themes to three Keep Catppuccin (default), Dracula, and Nord; drop Gruvbox and One Dark. ThemeBundleRegistry.byId() falls back to the default for an unknown id, so a previously-persisted gruvbox/one-dark selection degrades gracefully to Catppuccin. Verified on device (Odin2): the Theme bundle picker lists exactly three themes and selection still works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add "hide status bar with keyboard" setting A new app-wide toggle (Settings → Appearance → Immersive keyboard, off by default) hides the system status bar while the soft keyboard is open, reclaiming vertical space for the editor and terminal — useful in landscape on small screens. The bar returns when the keyboard closes and can be swiped down to reveal while hidden. - MainViewModel: hide_status_bar_with_keyboard pref (default false) + setter. - JCodeShell: StatusBarKeyboardController watches WindowInsets.isImeVisible and drives the WindowInsetsController (hide/show statusBars, transient-by-swipe behavior); restores the bar on dispose. Wired into JCodeApp; the toggle threads to SettingsFeature. - feature/settings: "Immersive keyboard" card with the toggle. Verified on device (Odin2): with the toggle on, focusing the editor hides the status bar (top bar slides to the top edge) and dismissing the keyboard restores it; off by default leaves the bar visible. Works for any soft keyboard, so the terminal is covered too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Remove the Overview section from Settings The "Overview" section (the read-only "Effective config" summary card) duplicated values the editor and the editable cards below already show, so drop it. Settings now opens straight to Appearance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Open + focus a file at line:col tapped in the terminal Tapping a path in the terminal already opened the file, but stripped any `:line` / `:line:col` suffix. Now it parses them (1-based, as compilers/grep emit) and focuses the editor there. - EditorState: a RevealRequest (0-based line/col) that the view consumes once it's laid out with correct metrics (a fresh tab has no viewport line height yet); requestReveal/clearReveal. - EditorView: on a reveal request, place the caret at the line/col, scroll it a couple of lines below the top (context above), and focus the view. Line height is computed from the render config so it's correct even before first layout. - MainViewModel: openFileByGuestPath parses the optional :line:col, threads it to openLocalFile -> requestReveal (works for an already-open tab too), and emits bringEditorToFront. - JCodeShell: collects bringEditorToFront and, in modal layouts (e.g. mobile landscape where the terminal is a drawer over the editor), closes the terminal so the editor is visible. Verified on device (Odin2, mobile landscape): tapping /workspace/runtes/package.json:10:3 opened package.json, closed the terminal drawer, scrolled to line 10, and put the caret at 10:3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add terminal `code`/`jcode` command to open files in the editor A guest `code <path>[:line[:col]]` (alias `jcode`) command now opens and focuses a file in the editor, like VS Code's `code` CLI. The command is a shell function installed into the rootfs at /etc/profile.d/jcode-open.sh that prints a custom OSC 7711 escape with the (PWD-resolved) path. The session reader scans the PTY output for that escape and routes the path token to the existing openFileByGuestPath, reusing line:col reveal and bring-to-front. The VT parser consumes the unknown OSC without printing. Verified on device: absolute + relative paths, existing-tab focus + new-tab open, line:col jump, terminal auto-closes, no stray output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Name terminal tabs after the running program Terminal tabs are now auto-named from the foreground program instead of a static "bash N" + manual rename. A bash hook (installed via the existing /etc/profile.d shell-integration script) emits OSC 7712 with the running command's basename on each command and "terminal" at the prompt; the session reader scans for it and updates the tab label live. Default is "terminal". The OSC 7711 open-file scanner is generalized into a single OscScanner that reports any (code, payload); the reader routes 7711 -> open file and 7712 -> tab title. Removed the static default and the manual Rename dialog/labelOverrides. Verified on device: default "terminal"; running `sleep 8` shows "sleep" then reverts; `code <file>` open-file still works. Run-pipeline tabs auto-name to the invoked wrapper ("bash") since run steps are shell scripts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Name run-pipeline tabs after the tool, not the bash wrapper Run terminals invoke each step as `bash run-*.sh`, so the interactive tab-title hook only saw "bash". Set BASH_ENV to the shell-integration script so non-interactive bash sources it too: the DEBUG trap then fires for the script's own commands, naming the tab after the actual tool (npm/vite/dotnet) — the outcome of the foreground-process option, without native /proc polling or its Android SELinux risk. Guarded the title hook with `[ -t 1 ]` so it stays silent when stdout is captured/piped (e.g. $(bash -c ...)), preventing OSC bytes from leaking into captured output. Verified on device: a Run client tab shows "npm" (was "bash") while serving Vite on :5173, with clean output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Unify long-press context menus into one compact component Every long-press context menu now uses a single CompactContextMenu (core:design): a top row of icon-only buttons for common verbs (copy/cut/paste/rename/delete/close) plus compact icon+label list items for the rest. Replaces six separate, inconsistent, text-only menus. - New CompactContextMenu + ContextAction in core:design; added menu icons (copy, cut, delete, open, rename, select-all, clear, definition, references, format) to the JCodeIcon set. - Migrated: Explorer file/folder, project list, workspace header, terminal tab, and editor menus. - TerminalView's native PopupMenu now routes its long-press to the host via onContextMenu, so it renders the same Compose menu (mirrors EditorView). Verified on device: all six menus show the hybrid layout consistently, positioned correctly (anchored and at-touch-point), destructive actions in the error color. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Make the right-drawer Hide button icon-only + add tooltips to icon buttons The right-drawer "Hide" text button is now an icon-only ChevronRight button. To keep icon-only controls discoverable, add a reusable JcTooltip (core:design) wrapping Material3 TooltipBox — its label shows on long-press (touch) or hover (pointer). Tooltips applied to every icon-only button, mostly via the shared helpers (WorkbenchIconActionButton, ToolbarIcon, the CompactContextMenu quick-action row) plus the inline ones (project/file MoreVert, breadcrumb up, editor and terminal tab +/x, RunDebug open/stop/run/configure). Existing contentDescriptions are reused as the tooltip text. Verified on device: Hide renders as an icon; long-pressing it (and a context-menu quick action) shows the title tooltip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add "Hide tab close button" setting (default off) A new Tabs setting hides the "×" on editor and terminal tabs to avoid accidental closes. Editor tabs gain a long-press → Close menu (terminal tabs already had one), so a tab is never stranded when the × is hidden. The flag is shared via a CompositionLocal (LocalTabCloseButtonSetting, carrying both value and setter) consumed by the deep tab UIs and the settings toggle — deliberately NOT threaded as JCodeShell params: that composable is at the ART verifier's register limit and two more params tipped it into a launch-time VerifyError. The local carries the setter too so the settings screen reads it without new params. Verified on device: launches cleanly; toggling on live-removes the × from editor + terminal tabs; long-press → Close still closes a tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add Save-button long-press pop-over (Undo/Redo/Discard/Save all) Long-pressing the top-bar Save button opens a compact icon-only pop-over with Undo, Redo, Discard (revert to disk), and Save all. Actions are carried via a LocalEditorSaveActions CompositionLocal rather than new JCodeShell params, which would trip the ART verifier register limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Show a round dot on editor tabs with unsaved changes The modified indicator was a small square before the title; make it a round dot in the trailing slot (taking the "×" spot, editor-style) so an unsaved tab reads clearly. The "×" still shows for clean, closeable tabs; closing is done from the tab's long-press menu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Auto-reload clean editor tabs when the file changes on disk A clean (non-dirty) tab now mirrors its file: when an external writer (e.g. an agent in the terminal) changes it, the editor reloads it. A dirty tab is never clobbered. Detection is by a (lastModified, size) signature polled on foreground-regain, a slow RESUMED tick, and tab switch — not FileObserver, since proot/terminal writes are in another mount namespace and don't fire the app's inotify. Reload goes through a new atomic EditorState.replaceAll(text, onlyIfClean) that runs as one transaction on the single-writer dispatcher, so a concurrent keystroke can't interleave or be clobbered. The signature is updated on save/discard so our own writes never self-trigger; a post-read stability check defers reloads while the file is still being written; and the reload notice is throttled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Top-bar action buttons: Run/Stop, debug + terminal popovers, shimmer/badge - Reorder top-bar actions to Save - Run - Terminal (then right-drawer toggle). - Run toggles to Stop while a run is active (wired to handleRun/handleStopRun). - Run long-press opens a debug pop-over: Rerun (re-invokes the run); Continue and Step Into/Over/Out are shown but disabled (no debug engine yet). - Terminal button shimmers while any session has a foreground process (OSC-7712 title != "terminal"), and shows a dot badge for new background instances (cleared once the terminal panel is viewed). - Terminal long-press lists live instances; picking one opens the right drawer focused on that session. ContextAction gains an `enabled` flag; CompactContextMenu greys/disables it. WorkbenchIconActionButton gains optional shimmer + badge. All new wiring threads through EditorWorkspace/WorkbenchTopBar (no new JCodeShell params). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Cap terminal tab labels to 8 chars; harden top-bar review findings - Terminal session tabs (right drawer) and the Terminal-button instance list now cap each label to 8 characters (e.g. "ASPNETCORE_ENVIRONMENT=Development" -> "ASPNETC…") so a long OSC command title can't stretch a tab. Review fixes for the top-bar change: - WorkbenchIconActionButton: create the shimmer InfiniteTransition unconditionally (Rules of Composition); its value is only read when shimmer is on, so idle buttons still don't recompose per frame. - Compute terminalBusy/terminalHasUnseen/terminalInstances via derivedStateOf so a busy-title update doesn't rebuild the top bar or churn a new list/frame. - Prune seenTerminalIds to live sessions in the tracking LaunchedEffect so it can't grow stale across terminal close / run teardown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Terminal: handle CNL/CPL (ESC[E / ESC[F) so live output redraws in place The native VT parser implemented cursor up/down (A/B) but not Cursor Next Line (E) or Cursor Previous Line (F). The .NET build / MSBuild terminal logger uses ESC[<n>F to return to the top of its live progress block before redrawing; with F unhandled the cursor never moved up, so each tick's duration ("(0.1s)", "(0.2s)", ...) landed on a new line instead of updating in place. Add E (down n, col 0) and F (up n, col 0), clamped to the scroll region like A/B. Device-verified with `dotnet build` (net10): the duration now updates on a single line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * App auto-versioning: VERSION.txt (1.0.0) + git-commit-count versionCode versionName now reads /VERSION.txt (semver source of truth, start 1.0.0); versionCode is the git commit count (monotonic, deterministic, offline). Both fall back safely when VERSION.txt or git is unavailable. Enable buildConfig so BuildConfig.VERSION_NAME exposes the running version for extension min/targetJCodeVersion compatibility checks (used by the .jext loader). No CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Install extensions only from verified .jext packages The marketplace + installer now use the .jext model instead of cloning extension repos: - fetchIndex parses the new marketplace.yaml schema (uniqueName, jext path, fingerprint, min/targetJCodeVersion); only entries that ship a .jext are listed (load-only-.jext). - install(entry, appVersion) downloads the entry's .jext, verifies it (.jext-manifest.json per-file sha256 + the order-independent package fingerprint, plus the index fingerprint), checks minJCodeVersion against the running app version, then extracts (files at the zip root — no top-dir strip) to filesDir/extensions/<uniqueName>/ and parses extension.jehm + extension.yaml. - Add installLocalJext(file) for sideloading. Drop the codeload repo-clone path. - MarketplaceEntry: drop repo; add jext/fingerprint/min/targetJCodeVersion. - MainViewModel passes BuildConfig.VERSION_NAME for the compatibility check. Device-verified end-to-end on Odin2: browse -> download .jext -> verify -> min-version check -> install -> open hello.js -> "lang: JavaScript" + highlighting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Editor: extend syntax highlighting to functions, variables, constants, properties, operators, annotations TokenPalette grows from 5 to 11 token classes. The tokenizer now classifies identifiers as keyword / type / function-call / constant / property / variable (via call-paren and member-dot lookahead and an ALL_CAPS constant heuristic), and colors operators and @annotations. Pure app-side change driven by each LanguagePack's existing rules, so it benefits every installed language pack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extensions: show marketplace/UI icons with a dependency-free loader + monogram fallback MarketplaceEntry carries the marketplace-published icon URL (dist/icons/<id>.png), InstalledExtension resolves its shipped icon file from the extracted package. ManagerListRow/ManagerDetailScreen gain an additive optional leading slot (SDK/LSP managers unaffected). A new ExtensionIcon composable decodes the PNG off-thread via BitmapFactory (local file for installed, cached remote fetch for marketplace) and falls back to a type-tinted monogram when no asset is present, so every row always shows an icon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Editor: make the language-pack formatter real (Format Document action) The "Format" action no longer shows "coming soon". A new dependency-free CodeFormatter applies the active language pack's basic rules — trim trailing whitespace, convert leading tabs to `indent` spaces, ensure a single final newline (LF/CRLF preserved). MainViewModel.formatActiveTab() resolves the language pack for the focused tab and applies the result as one undoable applyEdit. Wired via the existing LocalEditorSaveActions CompositionLocal (no new JCodeShell param, per the ART verifier limit). EditorLanguageAction FormatSelection -> FormatDocument; the semantic actions still require an LSP. Device-verified: tab->spaces, trailing trim, and final-newline insertion all applied to a JavaScript file via the editor context menu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Editor: real as-you-type completions + snippet helpers from language packs Wires the orphaned core/editor-completion framework into the editor. EditorView now computes the identifier prefix at the caret after every edit/caret move and emits a CompletionAnchor (prefix + byte range + pixel position); a generic replaceRange() accepts a chosen item. EditorViewHost overlays CompletionWindow, sourcing items via the new LocalCompletionSource CompositionLocal, which the app fills from the focused file's language pack (completions, helpers, keywords, types) through languagePackCompletionItems(). Pack completions and helpers carry LSP snippet syntax, expanded via SnippetEngine — whose tab-stop offsets are now recorded during parse (the old indexOf-based apply mislocated empty stops like $0). CompletionWindow finally honors its anchor coordinates via a PopupPositionProvider (flips above when there's no room below). Device-verified: typing/placing the caret after "c" in a .js file pops keywords + the "cl" console.log snippet; accepting expands "console.log()" with the caret placed inside the parens ($0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Editor: fix three review findings in the formatter + completions - CodeFormatter no longer collapses an all-whitespace file to empty: with insertFinalNewline it now always re-adds exactly one trailing newline. - SnippetEngine.apply converts each tab stop's recorded CHARACTER index to a UTF-8 BYTE offset before shifting by the (byte) insertion offset, so caret placement is correct for non-ASCII snippets (not just ASCII). - EditorView.replaceRange keeps suppressAnchorUpdate set through the whole sync (updateImeCursor + restartInput + dismiss) so accepting a completion can never transiently re-open the popup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * App: apply the JCode launcher icon Replaces the default system icon with the JCode logo. Adds legacy mipmaps (ic_launcher / ic_launcher_round at all densities) plus an adaptive icon (ic_launcher_foreground + a #151922 background color) so it renders cleanly under any launcher mask. Manifest now points android:icon/roundIcon at @mipmap/ic_launcher. Device-verified in the app drawer (circular mask). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extensions: show the publisher/author (channel) in the marketplace + UI MarketplaceEntry and InstalledExtension carry an `author`, read from the index `publisher` (remote) and each extension.yaml `publisher` (installed). The detail header subtitle leads with "by <author>", and the marketplace / installed list rows prefix their description with it. Device-verified: the C# pack shows "by jcode" on its detail page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * App: dark themed splash screen (no more white flash) The app had no splash/launch theme and ran on Theme.Material.Light, so cold start flashed a white window and the Android 12+ system splash drew the icon on white before the dark IDE. Theme.JCode now sets a dark windowBackground + status/nav colors (@color/ic_launcher_background #151922) and, on API 31+, the splash background + the JCode logo as windowSplashScreenAnimatedIcon; the manifest applies it. Device-verified: cold start shows a dark splash with the centered logo, cross-fading straight into the dark editor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Output panel: real build/run log (teed from run terminals) + app activity The right-drawer Output tab was a static placeholder; make it a real, read-only, auto-following log. New OutputLog bus collects: run lifecycle headers (handleRun), the app message stream (emitMessage), and the actual build output teed from the run terminals — TerminalSessionManager gained an onOutput(id, buffer, len) hook that TerminalSessionHost routes into OutputLog, which assembles lines and strips ANSI/VT escapes. OutputSidebarContent renders it (monospace, colored by kind, auto-follow only when pinned to bottom, Clear button). Two bugs fixed during device verification: OutputLog.captured must be @Volatile (the PTY reader thread read a stale empty set and dropped all teed output); and cleanLine must drop the trailing CR before its \r-overwrite collapse, else CRLF terminal lines blank out entirely. Device-verified: Run an ASP.NET+Vite project → Output shows the run header, the dotnet build log, and the npm/Vite log, ANSI-stripped and correctly lined. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Managers: shared header (Search/Refresh + "N installed") + flat installed-first lists Redesign the Extensions, SDK, and LSP manager panels to a consistent layout. New shared ManagerPanelHeader (core/design): title + icon-only Search and Refresh buttons, an "N installed" count, and a toggle-to-filter search field. Each panel now renders ONE flat list sorted installed/updatable-first then by name, filtered by the search query — replacing the old per-category section cards (SDK/LSP) and the split Marketplace/Installed sections (Extensions). For SDK/LSP the category is shown as each row's subtitle so grouping info isn't lost. Device-verified: Extensions shows "4 installed" + installed-first list + live search filter; SDK shows "2 installed" with Node/Python on top + category subtitles; LSP shows "0 installed" + category subtitles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Extensions: drop the built-in Theme/Icon bundle sections (they live in Settings) Theme and icon bundles are built-in app customizations, not marketplace extensions, and are already fully managed in Settings → Appearance. Showing them in the Extensions list was redundant and out of place, so remove the two sections (and the now-dead BundleGalleryRow + the themeBundleId/onSelectTheme/iconBundleId/ onSelectIcon params + their call-site args + unused imports). The Extensions panel is now purely the installed/marketplace extension list. If theme/icon packs ever become installable extensions (the .jext spec reserves type: theme / type: icons), they'll appear in the list naturally as installed packs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0bd6ec7 commit 67a96c3

329 files changed

Lines changed: 36949 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.

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
android:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Set up JDK 21
16+
uses: actions/setup-java@v4
17+
with:
18+
distribution: temurin
19+
java-version: '21'
20+
21+
- name: Set up Rust stable
22+
uses: dtolnay/rust-toolchain@stable
23+
24+
- name: Cache Gradle
25+
uses: actions/cache@v4
26+
with:
27+
path: |
28+
~/.gradle/caches
29+
~/.gradle/wrapper
30+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
31+
restore-keys: |
32+
${{ runner.os }}-gradle-
33+
34+
- name: Cache Cargo
35+
uses: actions/cache@v4
36+
with:
37+
path: |
38+
~/.cargo/registry
39+
~/.cargo/git
40+
target
41+
key: ${{ runner.os }}-cargo-${{ hashFiles('native/**/Cargo.lock', 'native/**/Cargo.toml') }}
42+
restore-keys: |
43+
${{ runner.os }}-cargo-
44+
45+
- name: Cache Android SDK / NDK / Konan
46+
uses: actions/cache@v4
47+
with:
48+
path: |
49+
~/.android
50+
~/android-sdk
51+
~/.konan
52+
key: ${{ runner.os }}-android-sdk-ndk-r27c
53+
54+
- name: Enable KVM permissions
55+
run: |
56+
sudo chown $USER /dev/kvm || true
57+
58+
- name: Create detekt placeholder if missing
59+
run: ./gradlew tasks --all
60+
61+
- name: Build and verify
62+
uses: reactivecircus/android-emulator-runner@v2
63+
with:
64+
api-level: 34
65+
arch: x86_64
66+
profile: pixel_6
67+
script: ./gradlew assembleDebug lintDebug testDebugUnitTest detekt connectedDebugAndroidTest

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ google-services.json
3232

3333
# Android Profiling
3434
*.hprof
35+
AGENTS.md
36+
37+
# Local scratch (screenshots, junk)
38+
temp/

VERSION.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0.0

app-launch.png

1.63 MB
Loading

app/build.gradle.kts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.kotlin.compose)
5+
}
6+
7+
// Auto-versioning (no CI): versionName is the semver in /VERSION.txt (start 1.0.0);
8+
// versionCode is the git commit count (monotonic, deterministic, offline). Both
9+
// degrade to safe fallbacks when VERSION.txt or git is unavailable.
10+
val jcodeVersionName: String = runCatching {
11+
rootProject.file("VERSION.txt").readText().trim()
12+
}.getOrNull()?.takeIf { it.isNotBlank() } ?: "1.0.0"
13+
14+
val jcodeVersionCode: Int = runCatching {
15+
val p = ProcessBuilder("git", "rev-list", "--count", "HEAD")
16+
.directory(rootProject.projectDir)
17+
.redirectErrorStream(true)
18+
.start()
19+
val out = p.inputStream.bufferedReader().readText().trim()
20+
p.waitFor()
21+
out.toIntOrNull()
22+
}.getOrNull()?.takeIf { it > 0 } ?: 1
23+
24+
android {
25+
namespace = "dev.jcode"
26+
compileSdk = 36
27+
28+
defaultConfig {
29+
applicationId = "dev.jcode"
30+
minSdk = 28
31+
targetSdk = 28
32+
versionCode = jcodeVersionCode
33+
versionName = jcodeVersionName
34+
35+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
36+
}
37+
38+
buildTypes {
39+
release {
40+
isMinifyEnabled = false
41+
proguardFiles(
42+
getDefaultProguardFile("proguard-android-optimize.txt"),
43+
"proguard-rules.pro"
44+
)
45+
}
46+
}
47+
48+
compileOptions {
49+
sourceCompatibility = JavaVersion.VERSION_21
50+
targetCompatibility = JavaVersion.VERSION_21
51+
}
52+
53+
kotlinOptions {
54+
jvmTarget = "21"
55+
}
56+
57+
buildFeatures {
58+
compose = true
59+
buildConfig = true
60+
}
61+
62+
ndkVersion = "27.2.12479018"
63+
}
64+
65+
dependencies {
66+
// Compose
67+
implementation(platform(libs.compose.bom))
68+
implementation(libs.compose.ui)
69+
implementation(libs.compose.ui.graphics)
70+
implementation(libs.compose.ui.tooling.preview)
71+
implementation(libs.compose.material3)
72+
implementation(libs.compose.material3.adaptive)
73+
implementation(libs.compose.material.icons.extended)
74+
75+
// AndroidX
76+
implementation(libs.core.ktx)
77+
implementation(libs.appcompat)
78+
implementation(libs.lifecycle.runtime.ktx)
79+
implementation(libs.lifecycle.runtime.compose)
80+
implementation(libs.activity.compose)
81+
implementation(libs.datastore.preferences)
82+
83+
// Core modules
84+
implementation(project(":core:design"))
85+
implementation(project(":core:adaptive"))
86+
implementation(project(":core:resource"))
87+
implementation(project(":core:fs"))
88+
implementation(project(":core:buffer"))
89+
implementation(project(":core:editor"))
90+
implementation(project(":core:editor-decor"))
91+
implementation(project(":core:editor-completion"))
92+
implementation(project(":core:treesitter"))
93+
implementation(project(":core:term"))
94+
implementation(project(":core:distro"))
95+
implementation(project(":core:lsp"))
96+
implementation(project(":core:ctags"))
97+
implementation(project(":core:debug"))
98+
implementation(project(":core:vcs"))
99+
implementation(project(":core:search"))
100+
implementation(project(":core:config"))
101+
implementation(project(":core:ext"))
102+
implementation(project(":core:state"))
103+
104+
// Feature modules
105+
implementation(project(":feature:explorer"))
106+
implementation(project(":feature:editor-pane"))
107+
implementation(project(":feature:terminal-pane"))
108+
implementation(project(":feature:problems"))
109+
implementation(project(":feature:scm"))
110+
implementation(project(":feature:search"))
111+
implementation(project(":feature:debug"))
112+
implementation(project(":feature:settings"))
113+
implementation(project(":feature:sdk-manager"))
114+
implementation(project(":feature:lsp-manager"))
115+
implementation(project(":feature:marketplace"))
116+
implementation(project(":feature:onboarding"))
117+
118+
// Native modules
119+
implementation(project(":native:core"))
120+
implementation(project(":native:buffer"))
121+
implementation(project(":native:editor-render"))
122+
implementation(project(":native:tree-sitter"))
123+
implementation(project(":native:libgit2"))
124+
implementation(project(":native:ripgrep-ffi"))
125+
implementation(project(":native:pty"))
126+
implementation(project(":native:vt"))
127+
implementation(project(":native:wasmtime-ffi"))
128+
implementation(project(":native:grammars"))
129+
implementation(project(":native:proot"))
130+
131+
// Debug
132+
debugImplementation(libs.compose.ui.tooling)
133+
debugImplementation(libs.ui.test.manifest)
134+
135+
// Test
136+
testImplementation(libs.androidx.test.ext.junit)
137+
androidTestImplementation(platform(libs.compose.bom))
138+
androidTestImplementation(libs.ui.test.junit4)
139+
androidTestImplementation(libs.androidx.test.runner)
140+
androidTestImplementation(libs.androidx.test.espresso.core)
141+
}

app/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Add project specific ProGuard rules here.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.jcode
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import dev.jcode.native.buffer.BufferNativeModule
5+
import dev.jcode.native.editorrender.EditorRenderNativeModule
6+
import dev.jcode.native.libgit2.Libgit2NativeModule
7+
import dev.jcode.native.pty.PtyNativeModule
8+
import dev.jcode.native.ripgrepffi.RipgrepFfiNativeModule
9+
import dev.jcode.native.treesitter.TreeSitterNativeModule
10+
import dev.jcode.native.vt.VtNativeModule
11+
import dev.jcode.native.wasmtimeffi.WasmtimeFfiNativeModule
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class NativeLibrariesSmokeTest {
17+
18+
@Test
19+
fun loadsAllNativeLibraries() {
20+
BufferNativeModule.loadLibrary()
21+
EditorRenderNativeModule.loadLibrary()
22+
TreeSitterNativeModule.loadLibrary()
23+
Libgit2NativeModule.loadLibrary()
24+
RipgrepFfiNativeModule.loadLibrary()
25+
PtyNativeModule.loadLibrary()
26+
VtNativeModule.loadLibrary()
27+
WasmtimeFfiNativeModule.loadLibrary()
28+
}
29+
}

app/src/main/AndroidManifest.xml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
5+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
6+
<uses-permission android:name="android.permission.INTERNET" />
7+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8+
<uses-permission android:name="android.permission.WAKE_LOCK" />
9+
<!-- Legacy external storage (targetSdk 28) so the workspace bind to /storage/emulated/0 works. -->
10+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
11+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
12+
13+
<application
14+
android:requestLegacyExternalStorage="true"
15+
android:allowBackup="false"
16+
android:icon="@mipmap/ic_launcher"
17+
android:roundIcon="@mipmap/ic_launcher_round"
18+
android:label="J Code"
19+
android:supportsRtl="true"
20+
android:theme="@style/Theme.JCode">
21+
22+
<activity
23+
android:name=".MainActivity"
24+
android:exported="true"
25+
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode|density|fontScale|locale|layoutDirection"
26+
android:windowSoftInputMode="adjustResize">
27+
<intent-filter>
28+
<action android:name="android.intent.action.MAIN" />
29+
<category android:name="android.intent.category.LAUNCHER" />
30+
</intent-filter>
31+
</activity>
32+
33+
<service
34+
android:name=".BackendService"
35+
android:exported="false"
36+
android:foregroundServiceType="specialUse"
37+
android:stopWithTask="false">
38+
<property
39+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
40+
android:value="interactive_terminal_and_build_runner" />
41+
</service>
42+
43+
</application>
44+
45+
</manifest>

0 commit comments

Comments
 (0)