Skip to content
Merged
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ build/
*.iml
local.properties
.DS_Store

# Eclipse/Kotlin-LSP build output. Regenerated on every IDE build; never tracked.
bin/
.kotlin/
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

**Codebase (Dynamic)** (`ai.rever.boss.plugin.dynamic.codebase`) is a dynamic plugin for the BOSS desktop application.

Browse and explore project files
FILES / SEARCH / GIT for the current project.

- **Plugin ID**: `ai.rever.boss.plugin.dynamic.codebase`
- **Main Class**: `ai.rever.boss.plugin.dynamic.codebase.CodebaseDynamicPlugin`
- **API Version**: 1.0.20
- **API Version**: 1.0.87 (`minApiVersion`); BOSS >= 9.5.7

## Essential Commands

Expand Down Expand Up @@ -36,7 +36,7 @@ build.gradle.kts → Build config + version (single source of truth)
- Entry point: `DynamicPlugin` interface with `register(context)` and `dispose()`
- UI: `PanelComponentWithUI` with `@Composable Content()`
- State: ViewModel pattern with `StateFlow`
- Providers from `PluginContext`: `workspaceDataProvider`, `splitViewOperations`, `contextMenuProvider`, `activeTabsProvider`
- Providers from `PluginContext`: `fileSystemDataProvider`, `splitViewOperations`, `contextMenuProvider`, `directoryPickerProvider`, `projectDataProvider`, `gitDataProvider`, `projectSearchProvider`
- Null-safe provider access: providers may be null, UI must handle gracefully

### Dependencies
Expand Down
81 changes: 73 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# BOSS Codebase

The project file tree, in the left sidebar.
FILES, SEARCH and GIT, in the left sidebar.

An IntelliJ-style lazy tree over the host's `FileSystemDataProvider`, with multi-select, a full
context menu, and a background watcher that keeps the tree honest when files change on disk.
It also backs the `codebase_*` MCP tools.
Three tabs over the host's providers: an IntelliJ-style lazy file tree, global search and
replace, and a changes accordion with a true lane commit graph. It backs the `codebase_*`,
`git_*` and `project_*` MCP tools, and as of 1.6.0 it supersedes the standalone git-status and
git-log plugins.

## What it does
## FILES

- **Lazy tree** with compacted middle packages, expand and collapse, and a per-node loading
state, so a deep project does not stall on open.
Expand All @@ -22,8 +23,36 @@ It also backs the `codebase_*` MCP tools.
- **Project switching** through `ProjectDataProvider` and a directory picker. With no project
open, the empty state offers "Open Project".

## SEARCH

Global content search and replace over the host's `ProjectSearchProvider`.

- **Include and exclude globs**, both applied *inside* the engine. Exclusion used to be a
filter over the returned list, which meant the result cap was reached before the exclude
ran - so excluding a busy directory quietly returned fewer results than existed.
- **Replace** is dry-run first: the confirmation sheet states what will change before anything
is written, and refuses to run at all while the search is capped, because "replace all" over
a truncated result set is a partial write with no error.
- Open buffers are searched and edited through the editor's undoable path; closed files are
written to disk.

## GIT

- **Changes accordion** over `GitDataProvider`: staged, unstaged and untracked groups, as a
list or a compacted tree, with stage / unstage / discard / open-diff row actions. Every
destructive action goes through a confirmation sheet.
- **Commit graph** with a real lane layout, ref pills, a branch picker, and row actions for
checkout, revert, cherry-pick and show-diff.
- **Commit box**, with an optional AI-drafted message built from what is actually staged. A
draft, never an automatic commit.
- **Agent Review** hands the fluck-agent a brief plus the uncommitted diff, bounded to an
inline budget. Commits already on the branch are not attached - the brief points the agent
at `git_diff_between` for those.

## MCP tools

`codebase_*` - the file tree:

| Tool | Purpose |
|---|---|
| `codebase_tree` | List the file tree under a path (depth 1-6, default 2) |
Expand All @@ -33,17 +62,53 @@ It also backs the `codebase_*` MCP tools.
| `codebase_projects` | List recent projects |
| `codebase_select_project` | Open or select a project by path |

`git_*` / `project_*` - absorbed from the retired git-status, git-log and search-replace
plugins. Names and schemas are carried over verbatim, so an agent that already knows them
needs no change:

| Tool | Permission | Purpose |
|---|---|---|
| `git_status` | - | Working-tree status in porcelain `XY path` form |
| `git_log` | - | Recent commits (limit clamped to 1..500) |
| `git_diff` | - | One file's working-tree or staged diff |
| `git_diff_all` | - | Every changed file as `STATUS path` |
| `git_diff_ref` | - | The changes a single commit introduced |
| `git_diff_between` | - | The diff between two refs |
| `project_search` | - | Search file contents across the project |
| `git_stage` | `git.write` | Stage a file |
| `git_unstage` | `git.write` | Unstage a file |
| `git_stage_all` | `git.write` | Stage everything changed |
| `git_unstage_all` | `git.write` | Unstage everything |
| `git_discard` | `git.write` | Discard working-tree changes (irreversible) |
| `git_checkout` | `git.write` | Check out a commit, branch or tag |
| `git_cherry_pick` | `git.write` | Cherry-pick a commit |
| `git_revert` | `git.write` | Revert a commit |
| `project_replace` | `project.replace` | Replace across an explicit file list, dry-run by default |

The RBAC split is deliberate. An empty `requiredPermissions` is not a neutral default: the
host's MCP registry exposes such a tool to every local session, including one where nobody is
signed in. Reads are open on that basis; anything that mutates git state or writes file
contents sits behind a grant. Admins bypass both. Refs arriving from an agent are checked
against the same `isSafeRef` rule the UI uses, so a leading dash cannot become a git flag.

On a host too old for `include_hidden`, `codebase_tree` still answers but adds a `note:` line
saying the flag was ignored, rather than silently returning a filtered tree.

## Requirements

- BOSS >= 9.2.20, boss-plugin-api >= **1.0.66** (`minApiVersion`), needed for the
`FileSystemDataProvider.scanDirectory(path, showHidden)` overloads.
- BOSS >= **9.5.7**, boss-plugin-api >= **1.0.87** (`minApiVersion`), for `GitDataProvider`,
`ProjectSearchProvider` and the ref-scoped `logGraphFor`.
- Host providers: `fileSystemDataProvider`, `contextMenuProvider`, `directoryPickerProvider`,
`splitViewOperations`, `projectDataProvider`.
`splitViewOperations`, `projectDataProvider`, `gitDataProvider`, `projectSearchProvider`.
Every one is optional at runtime: a missing provider degrades that tab to a hint, never a
crash.
- No external binaries.

**Release ordering.** BossConsole retires git-status and git-log at `codebase >= 1.6.0`, so
this version must ship in the same wave as - or before - the host build carrying that floor.
The failure mode if it lands late is silent: those two panels are hidden and nothing replaces
them.

All scanning goes through the host provider. The plugin-local `LocalFileScanner` was
deliberately retired, so there is no second code path that can disagree with the host.

Expand Down
30 changes: 23 additions & 7 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "ai.rever.boss.plugin.dynamic"
version = "1.5.8"
version = "1.6.0"

java {
toolchain {
Expand Down Expand Up @@ -109,18 +109,34 @@ tasks.register<Jar>("buildPluginJar") {
)
}

// Include compiled classes
// Compiled classes AND processed resources - sourceSets.main.output already
// carries build/resources/main, so the raw src/main/resources must NOT be
// added as well. With DuplicatesStrategy.EXCLUDE the first copy wins, so
// adding both shipped the correct manifest only because the processed one
// happened to be declared first. That is a live landmine now that
// plugin.json holds the 0.0.0-dev placeholder and the plugin reads its
// version back out of the jar at runtime: a reorder would ship a plugin
// reporting 0.0.0-dev to the host.
from(sourceSets.main.get().output)

// Include plugin manifest
from("src/main/resources")
}

// Sync version from build.gradle.kts into plugin.json (single source of truth)
tasks.processResources {
// The filter rewrites the manifest version at execution time; declaring the
// version as an input makes a version bump re-run the task. Without this the
// processed manifest stays UP-TO-DATE and the new jar carries the old version.
val pluginVersion = version.toString()
inputs.property("pluginVersion", pluginVersion)
filesMatching("**/plugin.json") {
filter { line ->
line.replace(Regex(""""version"\s*:\s*"[^"]*""""), """"version": "\$version"""")
// The LAMBDA overload of replace, so the replacement is used
// verbatim. The String overload hands it to java.util.regex.Matcher,
// where a backslash escapes and a dollar is a group reference: the
// previous form emitted a literal backslash and only produced the
// right output because Matcher then swallowed it. It worked for
// every semver by accident, and the manifest version is now
// load-bearing (CodebaseDynamicPlugin reads it back at runtime).
Regex(""""version"\s*:\s*"[^"]*"""").replace(line) { """"version": "$pluginVersion"""" }
}
}
}
Expand All @@ -139,6 +155,6 @@ tasks.register<Jar>("shadowJar") {
)
}
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
// Processed resources only - see buildPluginJar above.
from(sourceSets.main.get().output)
from("src/main/resources")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package ai.rever.boss.plugin.dynamic.codebase

import ai.rever.boss.plugin.api.GitFileStatusData

/**
* Prompt builder for the Agent Review feature (P7).
*
* Pure function so it is unit-testable: takes the changed-file listing plus
* whatever diff text was fetched (capped by the caller) and renders the
* review brief the Atlas agent receives.
*/
object AgentReviewPrompt {

/** Character budget the caller passes to the diff collector. */
const val INLINE_DIFF_BUDGET = 15_000

/** Marker the diff collector appends when a block had to be sliced. */
const val TRUNCATION_MARKER = "… [truncated]"

fun build(
projectPath: String,
staged: List<GitFileStatusData>,
unstaged: List<GitFileStatusData>,
diffText: String?,
/**
* Whether the collector left anything out - a sliced file, a file
* dropped for want of budget or past its file cap, or one whose diff
* would not fetch. Passed as a fact rather than sniffed back out of
* [diffText]: the marker only ever appeared on a file the collector
* sliced, so a run that simply stopped early looked complete.
*/
diffTruncated: Boolean = false,
/** Free text from the review panel; empty when the user gave none. */
instructions: String = "",
/** Quick keeps it to a short pass; Deep asks for a thorough one. */
deep: Boolean = false,
/**
* The ref the change is being reviewed toward, e.g. "main".
*
* The attached diff is the UNCOMMITTED change, never a diff against
* this ref - collecting `baseRef..HEAD` inline would blow the budget
* on any branch more than a few commits old. The brief says so, and
* points the agent at git_diff_between for the committed half, which
* it can fetch itself if the review needs it.
*/
baseRef: String = "",
): String {
val sb = StringBuilder()
if (baseRef.isNotBlank()) {
sb.appendLine(
"Review these uncommitted changes (index + working tree) ahead of " +
"landing them on `$baseRef`.",
)
sb.appendLine(
"Only the uncommitted change is attached. For the commits already on " +
"this branch, call git_diff_between with from=`$baseRef`, to=`HEAD`.",
)
} else {
sb.appendLine("Review the uncommitted changes in this project (index + working tree vs HEAD).")
}
sb.appendLine()
sb.appendLine("Focus: correctness, bugs, security, naming, and whether the change does what it claims.")
if (deep) {
sb.appendLine(
"Go deep: trace the affected call paths, consider concurrency, error handling, " +
"and edge cases, and check whether tests cover the change.",
)
} else {
sb.appendLine("Keep it quick: surface the issues that matter most, not an exhaustive audit.")
}
sb.appendLine("Be specific: cite file:line, say what is wrong, suggest a fix.")
sb.appendLine("Do not apply any edits; this is a review only.")
if (instructions.isNotBlank()) {
sb.appendLine()
sb.appendLine("The reviewer also asked:")
sb.appendLine(instructions.trim())
}
sb.appendLine()

if (staged.isEmpty() && unstaged.isEmpty()) {
sb.appendLine("There are no uncommitted changes in $projectPath.")
return sb.toString().trimEnd()
}

if (staged.isNotEmpty()) {
sb.appendLine("Staged files:")
staged.forEach { sb.appendLine(" [${it.indexStatus?.name ?: "?"}] ${it.path}") }
sb.appendLine()
}
if (unstaged.isNotEmpty()) {
sb.appendLine("Working-tree changes:")
unstaged.forEach { sb.appendLine(" [${it.workTreeStatus?.name ?: "?"}] ${it.path}") }
sb.appendLine()
}

if (diffText.isNullOrBlank()) {
sb.appendLine("The diff could not be fetched inline; use the git_diff / git_diff_all tools to read it.")
} else {
// Inline whatever the caller budgeted - do NOT re-test against
// INLINE_DIFF_BUDGET here. The collector truncates to fit but then
// appends a per-file header and the marker, so a truncated result
// lands a few characters OVER the budget. Re-testing would replace
// that text with a tool pointer and drop the diff exactly in the
// case the truncation exists to serve: one oversized file.
if (diffTruncated) {
sb.appendLine(
"This diff is INCOMPLETE - it was cut to fit an inline budget. " +
"Files below with no diff here, and any block ending in " +
"\"$TRUNCATION_MARKER\", must be read with git_diff before you " +
"draw conclusions about them.",
)
}
sb.appendLine("Full diff:")
sb.appendLine("```diff")
sb.appendLine(diffText)
sb.appendLine("```")
}
return sb.toString().trimEnd()
}
}
Loading
Loading