Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/ide-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
implementation(project(":agent-ui")) // the agent's Compose UI plugin (AgentUiPlugin) — paired with AgentPlugin in BuiltInPlugins
implementation(project(":vcs-impl")) // the Git engine (JGit working copy + the GitHub client + the account store)
implementation(project(":vcs-ui")) // the version-control Compose UI plugin (VcsUiPlugin) — paired with VcsPlugin in BuiltInPlugins
implementation(project(":zipcli-plugin")) // ZipCLI: the engine plugin + the `zipcli` agent tool (see AgentBackend)
implementation(project(":zipcli-ui")) // the ZipCLI terminal tab (ZipCliUiPlugin) — paired with ZipCliPlugin in BuiltInPlugins
// Opt-in usage analytics. `api` because AnalyticsService appears in IdeServicesBackend's (public)
// constructor signature, so a host wiring it (ide-android) needs the type on its compile classpath.
api(project(":analytics-api"))
Expand Down
2 changes: 1 addition & 1 deletion app/ide-core/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ java = ["src/test/kotlin"]

[dependencies]
api = [{ module = "ide-ui" }, { module = "interp-api" }, { module = "layout-preview-api" }, { module = "analytics-api" }, { module = "store-api" }]
implementation = [{ module = "platform-core" }, { module = "project-model-api" }, { module = "project-model-impl" }, { module = "language-api" }, { module = "lang-jdt" }, { module = "lang-java" }, { module = "lang-xml" }, { module = "lang-kotlin" }, { module = "lang-ksp" }, { module = "interp-impl" }, { module = "decompiler" }, { module = "index-api" }, { module = "index-impl" }, { module = "analysis-api" }, { module = "analysis-impl" }, { module = "block-api" }, { module = "block-impl" }, { module = "plugin-api" }, { module = "plugin-impl" }, { module = "build-api" }, { module = "build-engine" }, { module = "jvm-build" }, { module = "android-support" }, { module = "layout-preview-impl" }, { module = "deps-api" }, { module = "deps-impl" }, { module = "agent-impl" }, { module = "agent-mcp" }, { module = "agent-ui" }, { module = "vcs-impl" }, { module = "vcs-ui" }, { module = "store-impl" }, { module = "vfs-api" }, "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0", "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1", "kotlin-stdlib"]
implementation = [{ module = "platform-core" }, { module = "project-model-api" }, { module = "project-model-impl" }, { module = "language-api" }, { module = "lang-jdt" }, { module = "lang-java" }, { module = "lang-xml" }, { module = "lang-kotlin" }, { module = "lang-ksp" }, { module = "interp-impl" }, { module = "decompiler" }, { module = "index-api" }, { module = "index-impl" }, { module = "analysis-api" }, { module = "analysis-impl" }, { module = "block-api" }, { module = "block-impl" }, { module = "plugin-api" }, { module = "plugin-impl" }, { module = "build-api" }, { module = "build-engine" }, { module = "jvm-build" }, { module = "android-support" }, { module = "layout-preview-impl" }, { module = "deps-api" }, { module = "deps-impl" }, { module = "agent-impl" }, { module = "agent-mcp" }, { module = "agent-ui" }, { module = "vcs-impl" }, { module = "vcs-ui" }, { module = "zipcli-plugin" }, { module = "zipcli-ui" }, { module = "store-impl" }, { module = "vfs-api" }, "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0", "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1", "kotlin-stdlib"]
testImplementation = [{ platform = "org.junit:junit-bom:5.11.4" }, "org.jetbrains.kotlin:kotlin-test:2.4.0", { module = "test-support" }, "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0", { module = "store-impl" }, { module = "bench-support" }, "org.bouncycastle:bcpkix-jdk18on:1.78.1"]
10 changes: 10 additions & 0 deletions app/ide-core/src/main/kotlin/dev/ide/core/agent/AgentBackend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import dev.ide.core.AI_CREDITS_SERVICE
import dev.ide.core.BackendContext
import dev.ide.core.IdeServices
import dev.ide.platform.log.Log
import dev.ide.zipcli.plugin.ZipCliAgentTool
import dev.ide.zipcli.plugin.ZipCliPlugin
import dev.ide.ui.backend.AgentService
import dev.ide.ui.backend.UiAgentAttachment
import dev.ide.ui.backend.UiAgentAttachmentKind
Expand Down Expand Up @@ -180,6 +182,14 @@ internal class AgentBackend(private val ctx: BackendContext) : AgentService {
}
}

/** ZipCLI over the open project (see the ZipCLI plugin). In-app chat only: deliberately absent from
* [mcpTools], whose allow-all gate would hand a remote client a delete/overwrite command line. */
private val zipCliTool = ZipCliAgentTool(
root = { ctx.servicesOrNull?.workspaceRoot },
enabled = { ctx.manager?.env?.pluginCatalog?.isEnabled(ZipCliPlugin.ID) != false },
)

private val tools = SimpleToolRegistry(builtinTools(checkpoints) + ftpControlTool + zipCliTool)
// --- ask_user: the agent's multiple-choice questions, answered in the chat ---

private val _questionRequest = MutableStateFlow<dev.ide.ui.backend.UiAgentQuestionRequest?>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private const val MAX_TEXT_BYTES = 5_000_000L
internal class FileBackend(private val ctx: BackendContext) : FileService {

override val fileSystemEpoch: StateFlow<Int> get() = ctx.fileSystemEpoch
override fun notifyExternalChange() = ctx.bumpFileSystemEpoch()

override fun fileTree(mode: TreeViewMode): TreeNode {
// Picker-safe: with no project open (the lazy-start path), the engine-backed tree builders would hit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.ide.core.plugins

import dev.ide.agent.ui.AgentUiPlugin
import dev.ide.zipcli.plugin.ZipCliPlugin
import dev.ide.zipcli.ui.ZipCliUiPlugin
import dev.ide.analysis.ACTION_PROVIDER_EP
import dev.ide.analysis.ANALYZER_EP
import dev.ide.analysis.MODULE_ANALYSIS
Expand Down Expand Up @@ -243,6 +245,9 @@ object BuiltInPlugins {
BuiltInPlugin(AgentPlugin(), ui = AgentUiPlugin),
// Version control: engine facet (settings page + VcsBackend wiring) + its Compose Git UI.
BuiltInPlugin(VcsPlugin(), ui = VcsUiPlugin),
// ZipCLI: file / search-and-replace / ZIP commands, for the AI agent (the `zipcli` tool) and for you
// (the terminal tab in the bottom console).
BuiltInPlugin(ZipCliPlugin(), ui = ZipCliUiPlugin),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ interface FileService {

/** Bumps whenever a file is created/imported/removed, so the UI re-reads [fileTree]. */
val fileSystemEpoch: StateFlow<Int> get() = MutableStateFlow(0)

/** A tool outside the IDE's own file operations (a terminal plugin) may have changed files on disk: bump
* [fileSystemEpoch] so the tree and open tabs re-read. A host with nothing to refresh keeps the no-op. */
fun notifyExternalChange() {}
}

// ---------------------------------------------------------------------------
Expand Down
79 changes: 79 additions & 0 deletions docs/zipcli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ZipCLI in CodePrimer

ZipCLI (the Android file / search-and-replace / ZIP command line, with its scripting language) is built in as a
plugin with two entry points, both over the **open project** (its root appears as `/project`):

- a **ZipCLI terminal tab** in the bottom console, beside Log / Problems / App log;
- a `zipcli` **tool for the AI agent**. Typical uses: bulk `replace` across a source tree, `find` /
`search` with ZipCLI's smart-case and regex handling, `zip` / `unzip` of assets or release bundles, and
re-runnable `.zcli` scripts (see `services/zipcli-core/docs/SCRIPTING.md` and `examples/backup.zcli`).

## Layout

| Path | What it is | Who edits it |
|---|---|---|
| `services/zipcli-core/src/main/kotlin/com/zipcli/` | ZipCLI's platform-free sources, copied verbatim | only `sync-from-zipcli.sh` |
| `services/zipcli-core/src/main/kotlin/dev/ide/zipcli/ZipCliProject.kt` | CodePrimer glue: mounts a project directory at `/project`, runs scripts non-interactively | us |
| `services/zipcli-core/tests/`, `docs/`, `examples/`, `CHANGELOG.md` | ZipCLI's own tests and docs, copied verbatim | only `sync-from-zipcli.sh` |
| `services/zipcli-plugin/` | `ZipCliPlugin` (identity, Settings > Plugins toggle) and `ZipCliAgentTool` | us |
| `services/zipcli-ui/` | `ZipCliUiPlugin` (the terminal tab), `TerminalPanel` / `TerminalSession` (common Compose) | us |

Not brought over: the Android app shell and its terminal UI (replaced by `zipcli-ui`), the SAF (folder-grant) file system and `saf` command, and the
`mcp` server commands. CodePrimer has its own agent MCP server, and projects are plain directories.

## Shared files touched (the whole upstream-conflict surface)

| File | Change |
|---|---|
| `settings.gradle.kts` | 2 `include(...)` lines + 1 line in the `"services"` group list |
| `app/ide-core/build.gradle.kts` | 2 dependency lines |
| `app/ide-core/module.toml` | `zipcli-plugin` and `zipcli-ui` in the `implementation` list |
| `app/ide-core/.../plugins/BuiltInPlugins.kt` | 2 imports + 1 `BuiltInPlugin(ZipCliPlugin(), ui = ZipCliUiPlugin)` entry |
| `app/ide-ui-api/.../backend/BackendServices.kt` | `FileService.notifyExternalChange()`, a default no-op |
| `app/ide-core/.../backend/FileBackend.kt` | its override (bumps the file-system epoch) |
| `app/ide-core/.../agent/AgentBackend.kt` | 2 imports + the `zipCliTool` block + `+ zipCliTool` in `tools` |

Everything else is new files, so a merge can only conflict in these files. If upstream edits one of those lines,
keep both sides: the ZipCLI lines are independent of whatever upstream added next to them.

## Keeping up with upstream CodeAssist

```sh
git remote add upstream https://github.com/tyron12233/CodeAssist.git # once
git fetch upstream
git merge upstream/main # or: git rebase upstream/main
```

## Keeping up with ZipCLI

```sh
services/zipcli-core/sync-from-zipcli.sh /path/to/zipcli/app
git diff --stat services/zipcli-core # review what ZipCLI changed
services/zipcli-core/tests/run.sh # engine tests (needs kotlinc)
```

The script copies the same file set every time and stops with an error if a new ZipCLI file needs an Android
type, so the JVM module cannot silently break.

## The terminal tab

Open the console at the bottom of the editor and pick **ZipCLI**. Enter runs the line; Up / Down (or the two arrow
buttons) walk the history; **Stop** cancels the running command; **Clear** empties the scrollback; the
scrollback is selectable. Output streams as it is printed, and a destructive step shows a **Yes / No** strip and
waits for you. The working directory and variables persist, and each project has its own session, which keeps
running (and keeps its scrollback) while you look at another console tab. After every command the file tree and
open tabs are refreshed. Your own commands run immediately; only the agent's go through the permission prompt.
The input is one line: for several commands put them in a `.zcli` file and `run` it (heredocs and `edit` need
that or the agent tool).

## Behaviour and limits

- **Permission-gated.** `zipcli` is a mutating tool, so every call goes through the agent's permission prompt.
It is offered to the in-app chat only, never to the MCP server (that server runs allow-all).
- **Confirmations.** A destructive step that asks for confirmation stops the batch before it; the agent is told
which command is waiting and repeats with `confirm=true` only if you asked for that action.
- **Not in Undo.** ZipCLI writes to disk directly, so its changes are not part of a chat turn's checkpoint, and an
open editor tab may need a reload.
- **Symlinks.** `/project` confines `..` paths, but a symlink inside the project that points outside it is
followed by the file layer.
- **Turning it off.** Settings > Plugins > ZipCLI removes the tool.
82 changes: 82 additions & 0 deletions services/zipcli-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Changelog

## 1.1.0 — LLM-friendliness pass (DEV_RECOMMENDATIONS.md)

**Breaking: single quotes are now literal (POSIX).** `'C:\new\table.txt'` is written as typed; previously `\n`, `\t`, `\\` were escapes inside `'...'` and silently corrupted writes. Only `\'` is special. Double quotes are unchanged. Use `"a\nb"` or a heredoc where the old behaviour was relied on. Failures now exit with 2 (usage) or 3 (not found) instead of always 1, and every error line carries a code (`[E_NOT_FOUND] ...`).

P0
- `find --regex` works again: the smart-case helper is a hand-written scanner (no helper regex to fail on Android's engine), and pattern errors are reported as `Invalid pattern`.
- Internal faults are `[E_INTERNAL] ... this is a bug, not your script`, never `Syntax error`.
- New heredoc write path: `create|update f <<'EOF' ... EOF` (byte-exact; `<<-` strips tabs). POSIX `'it'\''s'` accepted. Unterminated-quote errors quote the line with a caret.
- Shell prompts (`$ ls`) and bare operators get one diagnosis naming the four missing features.
- `history` defaults to the last 20; MCP commands are recorded.
- Deleted the empty `ui/TermanScreen.kt`.

P1
- Confirmation in a non-interactive run stops the batch before the prompt (`stopped at line N: 1 command needs confirmation, M command(s) not run`, status 4, MCP `pending_confirmation`). Global `--yes`/`-y` on `delete`, `set -y`.
- `--json` (find, search, list, cat, replace, help), `--count-only`, `--offset`, per-command MCP `report`, `check` / `check_only` dry run, `info [--json]`, `help --json`.
- `saf`/`mcp` over MCP explain they are app-only; `saf list` is available read-only. Tool description states the boundaries.

P2
- `list` expands wildcards; find/search explain they do not. `replace --fail-on-no-match`. `set --reset`. Unknown flags list the real ones; not-found errors list the folder; "-m 6" style mistakes name the flag. Truncation footers name the continuing flag.

Tests: new `RecommendationTests` and golden transcripts (`tests/run.sh`).

## Unreleased — hardening pass (behaviour fixes)

Fixes are grouped by severity. No new dependencies were introduced, so module.toml is unchanged;
everything uses the Kotlin stdlib and the APIs already declared there.

### Silent wrong behaviour fixed

- Lexer: a lone `>`, `<`, `|` or `&` used as a word is now rejected with an explanation instead of
being handed to the command as text. Previously `echo hello > out.txt` printed "hello > out.txt"
and created nothing.
- Lexer: command substitution and arithmetic (dollar-paren forms) are now rejected with a hint to use
`set NAME=value`. Previously the text was passed through unchanged, so `echo \$((1+1))` printed
the expression literally.
- Metacharacters glued to other text stay literal, so regexes such as `a|b` keep working.

### Semantics

- delete: `-f` now also ignores paths that do not exist, matching POSIX `rm -f`. Backup scripts can
call `delete old.zip -f` without an existence guard. **Behaviour change.**
- zip: new `-o` / `--overwrite` replaces an existing archive (previously a hard error, which made
every second run of a backup script fail).
- find and search: a run with no matches now exits with status 1 (grep convention), so
`find TODO dir || echo none` takes the fallback branch. **Behaviour change.**
- CommandParser: a token that merely looks like a flag (`-- archive contents --`, `-3`) is treated as
a positional argument; only flag-shaped tokens are parsed as flags, so real typos still get a
"did you mean" hint that now also mentions the `--` escape.

### Documentation

- CommandSpec: new optional `exitStatus` field, rendered by `help <command>`.
Documented for delete, find, replace, search, test, run, zip and unzip.
- help scripting and docs/SCRIPTING.md: sections on unsupported constructs, quoting, leading dashes
and exit status.
- create/update: the --content help now says single quotes keep text literal.
- examples/backup.zcli: uses zip -o so it can be re-run.

### Tests

- tests/ScenarioTests.kt: the errexit tests now use a plain `delete nothere` (the failing case), and
new checks cover idempotent delete -f, rejection of redirection/pipes/arithmetic, dash-prefixed
text, grep-style no-match status, and zip -o.

### Not in this pass (proposed next steps)

- zip -o deletes the old archive before writing the new one; an atomic temp-file + move would survive
a failure in the middle of a zip.
- if/elif/else and while: the parser only knows `for`; the `test A && cmd1 || cmd2` idiom is the
usual trap. Needs parser + runner nodes and tests.
- Arithmetic expansion proper (counting files and sizes) instead of only an error.
- stat / wc / tail style aggregation commands.
- `run --dry-run` file-system write plan.
- list -l: a legend row for the leading type column (deferred because tests assert that output).

### Verification status

The change set was reviewed by reading the modified sources, but this environment has no Kotlin
compiler or Gradle, so nothing was compiled or executed. Run the JVM test programs in tests/ in CI
before merging.
11 changes: 11 additions & 0 deletions services/zipcli-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}

// zipcli-core — ZipCLI's pure-JVM command engine (parser, script runner, file / archive / search commands),
// mounted over a project directory. Everything under src/main/kotlin/com/zipcli is a verbatim copy of the
// ZipCLI app's platform-free sources (re-sync with tools/sync-zipcli.sh); only src/main/kotlin/dev/ide/zipcli
// is CodePrimer's own glue. No Android, Compose or IDE dependency, so it builds and tests on a plain JVM.
dependencies {
implementation(libs.kotlinx.coroutines.core)
}
Loading