ur-agent 1.44.10 → 1.45.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +14 -7
- package/dist/cli.js +27253 -20984
- package/docs/AGENT_FEATURES.md +7 -3
- package/docs/CONFIGURATION.md +7 -2
- package/docs/IDE.md +14 -12
- package/docs/USAGE.md +6 -2
- package/docs/providers.md +11 -1
- package/documentation/index.html +1 -1
- package/extensions/jetbrains-ur/README.md +22 -0
- package/extensions/jetbrains-ur/build.gradle.kts +34 -0
- package/extensions/jetbrains-ur/settings.gradle.kts +1 -0
- package/extensions/jetbrains-ur/src/main/kotlin/dev/urnexus/SendSelectionAction.kt +28 -0
- package/extensions/jetbrains-ur/src/main/kotlin/dev/urnexus/UrAcpClient.kt +97 -0
- package/extensions/jetbrains-ur/src/main/kotlin/dev/urnexus/UrToolWindowFactory.kt +27 -0
- package/extensions/jetbrains-ur/src/main/resources/META-INF/plugin.xml +17 -0
- package/extensions/vscode-ur-inline-diffs/out/extension.js +66 -21
- package/extensions/vscode-ur-inline-diffs/package.json +1 -1
- package/package.json +2 -1
package/docs/AGENT_FEATURES.md
CHANGED
|
@@ -71,12 +71,16 @@ ur auth gemini
|
|
|
71
71
|
ur auth antigravity
|
|
72
72
|
ur config set provider ollama
|
|
73
73
|
ur config set provider openai-compatible
|
|
74
|
-
ur config set model
|
|
74
|
+
ur config set model qwen2.5-coder:7b
|
|
75
75
|
ur config set base_url http://localhost:11434/v1
|
|
76
76
|
ur config set provider.fallback ollama
|
|
77
77
|
ur upgrade
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
The optional `provider.fallback` value is used only to print an explicit
|
|
81
|
+
recovery recommendation in provider diagnostics. It never silently or
|
|
82
|
+
automatically changes the active provider.
|
|
83
|
+
|
|
80
84
|
## v1.25.x Additions
|
|
81
85
|
|
|
82
86
|
| Addition | Surface | What it adds |
|
|
@@ -135,8 +139,8 @@ components instead of one giant prompt.
|
|
|
135
139
|
|
|
136
140
|
| Addition | Surface | What it adds |
|
|
137
141
|
| --- | --- | --- |
|
|
138
|
-
| Agent skill runner | `src/services/agents/agentSkillRunner.ts` | Reusable
|
|
139
|
-
| Worktree slash skills | `/debug-v2`, `/refactor`, `/paper-implementation`, `/benchmark`, `/security-review`, `/dockerize`, `/latex-paper` | Bundled slash skills
|
|
142
|
+
| Agent skill runner | `src/services/agents/agentSkillRunner.ts` | Reusable isolated-worktree wrapper that polls to completion; PR creation is available only with explicit `createPr: true`. |
|
|
143
|
+
| Worktree slash skills | `/debug-v2`, `/refactor`, `/paper-implementation`, `/benchmark`, `/security-review`, `/dockerize`, `/latex-paper`, `/batch` | Bundled slash skills keep changes local, run focused checks, ask before the final full suite, and never publish automatically. |
|
|
140
144
|
| Agent templates | `ur agent-templates install` | Adds `debug-v2`, `refactor`, `paper-implementation`, `benchmark`, `security-review`, `dockerize`, and `latex-paper` reusable agent templates under `.ur/agents/`. |
|
|
141
145
|
| Worktree command | `ur worktree list\|status\|clean` | Inspect and clean up UR agent worktrees created by `ur bg` or slash skills. |
|
|
142
146
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -58,6 +58,10 @@ ur provider select-model <provider> <model> --json
|
|
|
58
58
|
ur config set base_url <url>
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
`provider.fallback` is diagnostic recovery metadata, not automatic routing.
|
|
62
|
+
When the active provider fails, `ur provider doctor` shows the configured
|
|
63
|
+
recovery command; changing providers remains an explicit user action.
|
|
64
|
+
|
|
61
65
|
Provider values accept canonical IDs and common aliases. Examples:
|
|
62
66
|
`openai-api`, `anthropic-api`, `gemini-api`, `openrouter`, `ollama`,
|
|
63
67
|
`lmstudio`, `LM Studio`, `llama.cpp`, `vllm`, and the subscription CLI
|
|
@@ -88,6 +92,7 @@ it in the environment when you explicitly choose API mode:
|
|
|
88
92
|
|
|
89
93
|
```sh
|
|
90
94
|
OPENAI_API_KEY=...
|
|
95
|
+
OPENAI_COMPATIBLE_API_KEY=...
|
|
91
96
|
ANTHROPIC_API_KEY=...
|
|
92
97
|
GEMINI_API_KEY=...
|
|
93
98
|
OPENROUTER_API_KEY=...
|
|
@@ -117,8 +122,8 @@ ur --settings '{"ollama":{"host":"http://192.168.1.50:11434"}}'
|
|
|
117
122
|
Model selection environment variables still work the same way:
|
|
118
123
|
|
|
119
124
|
```sh
|
|
120
|
-
OLLAMA_MODEL=
|
|
121
|
-
UR_MODEL=
|
|
125
|
+
OLLAMA_MODEL=qwen2.5-coder:7b
|
|
126
|
+
UR_MODEL=qwen2.5-coder:7b
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
`OLLAMA_MODEL` selects the model name and takes precedence over `UR_MODEL` only
|
package/docs/IDE.md
CHANGED
|
@@ -4,8 +4,7 @@ UR's professional IDE integration is the **UR Inline Diffs** VS Code extension,
|
|
|
4
4
|
bundled in this repository (`extensions/vscode-ur-inline-diffs`). It gives VS
|
|
5
5
|
Code, Cursor, and Windsurf a chat panel, inline diff review, an actions panel,
|
|
6
6
|
an agent status card, a searchable command palette, and an agent options
|
|
7
|
-
panel — all driven by the same `ur` CLI you already use from a terminal
|
|
8
|
-
no separate server process and no new network surface.
|
|
7
|
+
panel — all driven by the same `ur` CLI you already use from a terminal.
|
|
9
8
|
|
|
10
9
|
Other editors connect through different mechanisms, chosen per editor and
|
|
11
10
|
stated honestly — nothing here claims support that does not exist:
|
|
@@ -19,12 +18,12 @@ stated honestly — nothing here claims support that does not exist:
|
|
|
19
18
|
Neovim clients. This remains a separate transport from the VS Code
|
|
20
19
|
extension's chat bridge below; the two are independent and neither depends
|
|
21
20
|
on the other.
|
|
22
|
-
- **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
- **JetBrains plugin** — the experimental bundled plugin under
|
|
22
|
+
`extensions/jetbrains-ur` connects to `ur acp serve` on loopback. It uses
|
|
23
|
+
JSON-RPC `initialize`, `session/new`, and `session/prompt` calls at `/acp`,
|
|
24
|
+
preserves a project-scoped session, and performs network work off the IDE UI
|
|
25
|
+
thread. It is installable from its `buildPlugin` zip; marketplace publishing
|
|
26
|
+
is not claimed.
|
|
28
27
|
|
|
29
28
|
## Supported targets
|
|
30
29
|
|
|
@@ -34,7 +33,7 @@ stated honestly — nothing here claims support that does not exist:
|
|
|
34
33
|
| Cursor | UR Inline Diffs extension (VS Code fork) | `.vscode/settings.json` | Same extension as VS Code. |
|
|
35
34
|
| Windsurf | UR Inline Diffs extension (VS Code fork) | `.vscode/settings.json` | Same extension as VS Code. |
|
|
36
35
|
| Zed | stdio ACP | `.zed/settings.json` | Real Agent Client Protocol over stdio. |
|
|
37
|
-
| JetBrains |
|
|
36
|
+
| JetBrains | bundled experimental plugin + HTTP ACP | manual zip install | Project-scoped JSON-RPC session and Send Selection action; synchronous ACP task result, not token streaming. |
|
|
38
37
|
| Neovim | stdio ACP | snippet | Requires a third-party ACP client plugin. |
|
|
39
38
|
| Generic ACP | stdio ACP / HTTP | snippet | `ur acp stdio` (native ACP) or `ur acp serve` (HTTP JSON-RPC; not a streaming transport — see Known limitations). |
|
|
40
39
|
|
|
@@ -128,7 +127,9 @@ rather than leaving it hanging.
|
|
|
128
127
|
`.ur/ide/chat/` in the workspace (a manifest plus one file per session).
|
|
129
128
|
**UR: Open Chat** can resume a previous session — using the CLI's own
|
|
130
129
|
`--resume` — or start a new one; sessions survive closing and reopening VS
|
|
131
|
-
Code.
|
|
130
|
+
Code. In multi-root workspaces the chat binds to the folder containing the
|
|
131
|
+
active editor, and stale callbacks from a canceled turn cannot mutate a newer
|
|
132
|
+
chat.
|
|
132
133
|
|
|
133
134
|
**File and selection context.** **UR: Add Current File to Chat** and
|
|
134
135
|
**UR: Add Selection to Chat** attach the active file or selection as explicit
|
|
@@ -200,8 +201,9 @@ success it did not observe.
|
|
|
200
201
|
|
|
201
202
|
## Known limitations
|
|
202
203
|
|
|
203
|
-
- **JetBrains is
|
|
204
|
-
|
|
204
|
+
- **JetBrains integration is experimental and non-streaming.** It uses the
|
|
205
|
+
loopback HTTP ACP server and returns the completed synchronous task output;
|
|
206
|
+
it does not yet render token or tool-call streams.
|
|
205
207
|
- **The lockfile/MCP IDE bridge is Phase 2 and not part of this MVP.** The
|
|
206
208
|
CLI already implements the *client* half of a lockfile-discovered MCP
|
|
207
209
|
connection to a running IDE (`src/utils/ide.ts`), used by the terminal
|
package/docs/USAGE.md
CHANGED
|
@@ -13,7 +13,7 @@ Use interactive mode for iterative coding, debugging, research, and repository e
|
|
|
13
13
|
Useful options:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
ur --model
|
|
16
|
+
ur --model qwen2.5-coder:7b
|
|
17
17
|
ur --add-dir ../other-project
|
|
18
18
|
ur --permission-mode ask
|
|
19
19
|
ur --continue
|
|
@@ -63,7 +63,7 @@ Ollama router choose from the models exposed by your local Ollama app.
|
|
|
63
63
|
You can also choose the model for a single session:
|
|
64
64
|
|
|
65
65
|
```sh
|
|
66
|
-
ur --model
|
|
66
|
+
ur --model qwen2.5-coder:7b
|
|
67
67
|
ur --model qwen2.5-coder:latest
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -108,6 +108,10 @@ ur config set base_url <url>
|
|
|
108
108
|
ur config set provider.fallback ollama
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
`provider.fallback` only controls the recovery suggestion printed by provider
|
|
112
|
+
diagnostics. UR does not switch or retry across providers automatically; use
|
|
113
|
+
`ur config set provider <id>` after reviewing the failure.
|
|
114
|
+
|
|
111
115
|
In the interactive app, `/model` chooses a provider first and then a model from
|
|
112
116
|
that provider only. The saved pair controls the runtime backend for the next
|
|
113
117
|
agent request. There is no cross-provider fallback: OpenAI API does not fall
|
package/docs/providers.md
CHANGED
|
@@ -36,6 +36,7 @@ multimodal input, external CLI boundary, and sandbox scope:
|
|
|
36
36
|
| Claude API | API | UR-native | no | yes | yes | yes | UR Bash/File sandbox | `api:anthropic` | `ANTHROPIC_API_KEY` |
|
|
37
37
|
| Gemini API | API | UR-native | no | yes | yes | yes | UR Bash/File sandbox | `api:gemini` | `GEMINI_API_KEY` |
|
|
38
38
|
| OpenRouter | API/router | UR-native | no | yes | yes | yes | UR Bash/File sandbox | `api:openrouter` | `OPENROUTER_API_KEY` |
|
|
39
|
+
| OpenAI-compatible | server/API | UR-native | no | yes | yes | endpoint-dependent | UR Bash/File sandbox | `openai-compatible` | optional `OPENAI_COMPATIBLE_API_KEY`; never reuses `OPENAI_API_KEY` |
|
|
39
40
|
| Ollama | local | UR-native | no | yes | yes | yes* | UR Bash/File sandbox | `ollama` | localhost Ollama runtime |
|
|
40
41
|
| LM Studio | local/server | UR-native | no | yes | yes | yes | UR Bash/File sandbox | `openai-compatible:lmstudio` | local OpenAI-compatible server |
|
|
41
42
|
| llama.cpp | local/server | UR-native | no | yes | yes | yes | UR Bash/File sandbox | `openai-compatible:llama.cpp` | local OpenAI-compatible server |
|
|
@@ -106,6 +107,10 @@ ur config set base_url <url>
|
|
|
106
107
|
ur config set provider.fallback ollama
|
|
107
108
|
```
|
|
108
109
|
|
|
110
|
+
The fallback setting is a recovery hint for `ur provider doctor`; it does not
|
|
111
|
+
route a failed request to another provider. Review the failure and use
|
|
112
|
+
`ur config set provider <id>` to switch explicitly.
|
|
113
|
+
|
|
109
114
|
## Provider-scoped model selection
|
|
110
115
|
|
|
111
116
|
UR-Nexus shows providers first, then only models available for the selected provider. This prevents incompatible model/provider pairs and keeps API-key, local/server, subscription, and external app bridge model lists separate. The generic `subscription` entry has no models unless a real independent subscription runtime is configured; UR does not list fake subscription models.
|
|
@@ -185,7 +190,7 @@ After selecting a model, the confirmation shows:
|
|
|
185
190
|
|
|
186
191
|
→ Step 2: Select model
|
|
187
192
|
llama3 · discovered from Ollama · live
|
|
188
|
-
|
|
193
|
+
qwen2.5-coder:7b · discovered from Ollama · live
|
|
189
194
|
|
|
190
195
|
→ Select: llama3
|
|
191
196
|
|
|
@@ -401,6 +406,7 @@ API providers require explicit user selection and environment keys:
|
|
|
401
406
|
|
|
402
407
|
```sh
|
|
403
408
|
OPENAI_API_KEY=...
|
|
409
|
+
OPENAI_COMPATIBLE_API_KEY=...
|
|
404
410
|
ANTHROPIC_API_KEY=...
|
|
405
411
|
GEMINI_API_KEY=...
|
|
406
412
|
OPENROUTER_API_KEY=...
|
|
@@ -414,6 +420,10 @@ ur config set base_url http://localhost:1234/v1
|
|
|
414
420
|
ur config set model local-model-name
|
|
415
421
|
```
|
|
416
422
|
|
|
423
|
+
`OPENAI_COMPATIBLE_API_KEY` is intentionally separate from
|
|
424
|
+
`OPENAI_API_KEY`; selecting an arbitrary compatible base URL never forwards
|
|
425
|
+
the OpenAI credential to that host.
|
|
426
|
+
|
|
417
427
|
Local/server providers use their normal endpoints:
|
|
418
428
|
|
|
419
429
|
- Ollama: `http://localhost:11434`
|
package/documentation/index.html
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<main id="content" class="content">
|
|
45
45
|
<header class="topbar">
|
|
46
46
|
<div>
|
|
47
|
-
<p class="eyebrow">Version 1.
|
|
47
|
+
<p class="eyebrow">Version 1.45.2</p>
|
|
48
48
|
<h1>UR-Nexus Documentation</h1>
|
|
49
49
|
<p class="lead">A practical, tutorial-style reference for installing, configuring, automating, extending, and operating UR-Nexus.</p>
|
|
50
50
|
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# UR for JetBrains (experimental scaffold)
|
|
2
|
+
|
|
3
|
+
Connects JetBrains IDEs to a running UR-Nexus agent over the local ACP server
|
|
4
|
+
(`ur acp serve --port 9100`). Builds with the IntelliJ Platform Gradle Plugin 2.x (verified: `gradle buildPlugin` produces `build/distributions/jetbrains-ur-<version>.zip` against IDEA 2024.2). Install via Settings → Plugins → ⚙ → Install Plugin from Disk. Mirrors what `extensions/vscode-ur-inline-diffs` does for VS Code.
|
|
5
|
+
|
|
6
|
+
## What it wires up
|
|
7
|
+
|
|
8
|
+
- A tool window ("UR Agent") that checks the loopback ACP endpoint without
|
|
9
|
+
blocking the IDE event thread.
|
|
10
|
+
- An action (Tools → UR: Send Selection) that posts the current selection as
|
|
11
|
+
a prompt to a project-scoped JSON-RPC session at `/acp`. The current server
|
|
12
|
+
returns a completed synchronous task result; token streaming is not claimed.
|
|
13
|
+
|
|
14
|
+
## Build
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
cd extensions/jetbrains-ur
|
|
18
|
+
JAVA_HOME=$(brew --prefix openjdk@17) gradle buildPlugin # → build/distributions/*.zip
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The heavy lifting stays in UR's ACP server — the plugin is intentionally a
|
|
22
|
+
thin client, so protocol changes land on the CLI side.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id("java")
|
|
3
|
+
id("org.jetbrains.kotlin.jvm") version "2.0.21"
|
|
4
|
+
id("org.jetbrains.intellij.platform") version "2.7.2"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
group = "dev.urnexus"
|
|
8
|
+
version = "1.45.2"
|
|
9
|
+
|
|
10
|
+
repositories {
|
|
11
|
+
mavenCentral()
|
|
12
|
+
intellijPlatform {
|
|
13
|
+
defaultRepositories()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dependencies {
|
|
18
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
|
|
19
|
+
intellijPlatform {
|
|
20
|
+
intellijIdeaCommunity("2024.2.3")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
intellijPlatform {
|
|
25
|
+
pluginConfiguration {
|
|
26
|
+
ideaVersion {
|
|
27
|
+
sinceBuild.set("242")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
kotlin {
|
|
33
|
+
jvmToolchain(17)
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rootProject.name = "jetbrains-ur"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package dev.urnexus
|
|
2
|
+
|
|
3
|
+
import com.intellij.openapi.actionSystem.AnAction
|
|
4
|
+
import com.intellij.openapi.actionSystem.AnActionEvent
|
|
5
|
+
import com.intellij.openapi.actionSystem.CommonDataKeys
|
|
6
|
+
import com.intellij.openapi.ui.Messages
|
|
7
|
+
import com.intellij.openapi.progress.ProgressIndicator
|
|
8
|
+
import com.intellij.openapi.progress.ProgressManager
|
|
9
|
+
import com.intellij.openapi.progress.Task
|
|
10
|
+
import com.intellij.openapi.application.ApplicationManager
|
|
11
|
+
|
|
12
|
+
class SendSelectionAction : AnAction() {
|
|
13
|
+
override fun actionPerformed(e: AnActionEvent) {
|
|
14
|
+
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
|
|
15
|
+
val selection = editor.selectionModel.selectedText ?: return
|
|
16
|
+
val project = e.project ?: return
|
|
17
|
+
val client = project.getService(UrAcpClient::class.java)
|
|
18
|
+
ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Sending selection to UR", true) {
|
|
19
|
+
override fun run(indicator: ProgressIndicator) {
|
|
20
|
+
val result = runCatching { client.sendPrompt(selection) }
|
|
21
|
+
ApplicationManager.getApplication().invokeLater {
|
|
22
|
+
result.onSuccess { Messages.showInfoMessage(project, it.take(2000), "UR Agent") }
|
|
23
|
+
.onFailure { Messages.showErrorDialog(project, it.message ?: it.toString(), "UR Agent") }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
package dev.urnexus
|
|
2
|
+
|
|
3
|
+
import com.intellij.openapi.components.Service
|
|
4
|
+
import com.intellij.openapi.project.Project
|
|
5
|
+
import java.net.URI
|
|
6
|
+
import java.net.http.HttpClient
|
|
7
|
+
import java.net.http.HttpRequest
|
|
8
|
+
import java.net.http.HttpResponse
|
|
9
|
+
import java.time.Duration
|
|
10
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
11
|
+
import kotlinx.serialization.json.Json
|
|
12
|
+
import kotlinx.serialization.json.JsonElement
|
|
13
|
+
import kotlinx.serialization.json.JsonObject
|
|
14
|
+
import kotlinx.serialization.json.JsonPrimitive
|
|
15
|
+
import kotlinx.serialization.json.buildJsonObject
|
|
16
|
+
import kotlinx.serialization.json.jsonObject
|
|
17
|
+
import kotlinx.serialization.json.jsonPrimitive
|
|
18
|
+
|
|
19
|
+
/** Project-scoped JSON-RPC client for UR's HTTP ACP endpoint. */
|
|
20
|
+
@Service(Service.Level.PROJECT)
|
|
21
|
+
class UrAcpClient(private val project: Project) {
|
|
22
|
+
private val baseUrl = "http://127.0.0.1:9100"
|
|
23
|
+
private val http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build()
|
|
24
|
+
private val ids = AtomicLong()
|
|
25
|
+
private val json = Json { ignoreUnknownKeys = true }
|
|
26
|
+
@Volatile private var initialized = false
|
|
27
|
+
@Volatile private var sessionId: String? = null
|
|
28
|
+
|
|
29
|
+
fun health(): Boolean = try {
|
|
30
|
+
val req = HttpRequest.newBuilder(URI.create("$baseUrl/healthz"))
|
|
31
|
+
.timeout(Duration.ofSeconds(5)).GET().build()
|
|
32
|
+
http.send(req, HttpResponse.BodyHandlers.ofString()).statusCode() == 200
|
|
33
|
+
} catch (_: Exception) { false }
|
|
34
|
+
|
|
35
|
+
@Synchronized
|
|
36
|
+
fun sendPrompt(prompt: String): String {
|
|
37
|
+
require(prompt.isNotBlank()) { "Prompt cannot be empty." }
|
|
38
|
+
ensureSession()
|
|
39
|
+
val result = rpc("session/prompt", buildJsonObject {
|
|
40
|
+
put("sessionId", JsonPrimitive(sessionId!!))
|
|
41
|
+
put("prompt", JsonPrimitive(prompt))
|
|
42
|
+
put("mode", JsonPrimitive("sync"))
|
|
43
|
+
}).jsonObject
|
|
44
|
+
val task = result["task"]?.jsonObject ?: error("ACP response did not include a task.")
|
|
45
|
+
val status = task["status"]?.jsonPrimitive?.content ?: "unknown"
|
|
46
|
+
val output = task["result"]?.jsonObject?.get("stdout")?.jsonPrimitive?.content
|
|
47
|
+
val failure = task["result"]?.jsonObject?.get("stderr")?.jsonPrimitive?.content
|
|
48
|
+
return output?.takeIf { it.isNotBlank() }
|
|
49
|
+
?: failure?.takeIf { it.isNotBlank() }
|
|
50
|
+
?: "UR task $status (${task["id"]?.jsonPrimitive?.content ?: "unknown id"})"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Synchronized
|
|
54
|
+
fun closeSession() {
|
|
55
|
+
val active = sessionId ?: return
|
|
56
|
+
try { rpc("session/cancel", buildJsonObject { put("sessionId", JsonPrimitive(active)) }) }
|
|
57
|
+
finally { sessionId = null }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private fun ensureSession() {
|
|
61
|
+
if (!initialized) {
|
|
62
|
+
rpc("initialize")
|
|
63
|
+
initialized = true
|
|
64
|
+
}
|
|
65
|
+
if (sessionId == null) {
|
|
66
|
+
val params = project.basePath?.let { cwd ->
|
|
67
|
+
buildJsonObject { put("cwd", JsonPrimitive(cwd)) }
|
|
68
|
+
}
|
|
69
|
+
sessionId = rpc("session/new", params).jsonObject["sessionId"]?.jsonPrimitive?.content
|
|
70
|
+
?: error("ACP session/new response did not include sessionId.")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun rpc(method: String, params: JsonObject? = null): JsonElement {
|
|
75
|
+
val requestId = ids.incrementAndGet()
|
|
76
|
+
val body = buildJsonObject {
|
|
77
|
+
put("jsonrpc", JsonPrimitive("2.0"))
|
|
78
|
+
put("id", JsonPrimitive(requestId))
|
|
79
|
+
put("method", JsonPrimitive(method))
|
|
80
|
+
if (params != null) put("params", params)
|
|
81
|
+
}
|
|
82
|
+
val request = HttpRequest.newBuilder(URI.create("$baseUrl/acp"))
|
|
83
|
+
.timeout(Duration.ofMinutes(31))
|
|
84
|
+
.header("content-type", "application/json")
|
|
85
|
+
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
|
86
|
+
.build()
|
|
87
|
+
val response = http.send(request, HttpResponse.BodyHandlers.ofString())
|
|
88
|
+
if (response.statusCode() !in 200..299) {
|
|
89
|
+
error("ACP HTTP ${response.statusCode()}: ${response.body().take(500)}")
|
|
90
|
+
}
|
|
91
|
+
val payload = json.parseToJsonElement(response.body()).jsonObject
|
|
92
|
+
payload["error"]?.jsonObject?.let { rpcError ->
|
|
93
|
+
error("ACP ${rpcError["code"]?.jsonPrimitive?.content}: ${rpcError["message"]?.jsonPrimitive?.content}")
|
|
94
|
+
}
|
|
95
|
+
return payload["result"] ?: error("ACP response did not include a result.")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package dev.urnexus
|
|
2
|
+
|
|
3
|
+
import com.intellij.openapi.project.Project
|
|
4
|
+
import com.intellij.openapi.wm.ToolWindow
|
|
5
|
+
import com.intellij.openapi.wm.ToolWindowFactory
|
|
6
|
+
import com.intellij.ui.components.JBScrollPane
|
|
7
|
+
import com.intellij.ui.content.ContentFactory
|
|
8
|
+
import javax.swing.JTextArea
|
|
9
|
+
import com.intellij.openapi.application.ApplicationManager
|
|
10
|
+
|
|
11
|
+
class UrToolWindowFactory : ToolWindowFactory {
|
|
12
|
+
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
|
13
|
+
val client = project.getService(UrAcpClient::class.java)
|
|
14
|
+
val text = JTextArea()
|
|
15
|
+
text.isEditable = false
|
|
16
|
+
text.text = "Checking UR ACP server..."
|
|
17
|
+
val content = ContentFactory.getInstance().createContent(JBScrollPane(text), "", false)
|
|
18
|
+
toolWindow.contentManager.addContent(content)
|
|
19
|
+
ApplicationManager.getApplication().executeOnPooledThread {
|
|
20
|
+
val status = if (client.health())
|
|
21
|
+
"Connected to UR ACP server (127.0.0.1:9100)."
|
|
22
|
+
else
|
|
23
|
+
"UR ACP server not running.\nStart it with: ur acp serve --port 9100"
|
|
24
|
+
ApplicationManager.getApplication().invokeLater { text.text = status }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<idea-plugin>
|
|
2
|
+
<id>dev.urnexus.jetbrains</id>
|
|
3
|
+
<name>UR Agent</name>
|
|
4
|
+
<vendor url="https://github.com/Maitham16/UR">UR-Nexus</vendor>
|
|
5
|
+
<description>Connects JetBrains IDEs to a local UR-Nexus agent over ACP (ur acp serve).</description>
|
|
6
|
+
<depends>com.intellij.modules.platform</depends>
|
|
7
|
+
<extensions defaultExtensionNs="com.intellij">
|
|
8
|
+
<toolWindow id="UR Agent" anchor="right"
|
|
9
|
+
factoryClass="dev.urnexus.UrToolWindowFactory"/>
|
|
10
|
+
</extensions>
|
|
11
|
+
<actions>
|
|
12
|
+
<action id="UR.SendSelection" class="dev.urnexus.SendSelectionAction"
|
|
13
|
+
text="UR: Send Selection" description="Send the current selection to the UR agent">
|
|
14
|
+
<add-to-group group-id="ToolsMenu" anchor="last"/>
|
|
15
|
+
</action>
|
|
16
|
+
</actions>
|
|
17
|
+
</idea-plugin>
|
|
@@ -62,7 +62,8 @@ var fs = __toESM(require("node:fs"));
|
|
|
62
62
|
var path = __toESM(require("node:path"));
|
|
63
63
|
var vscode2 = __toESM(require("vscode"));
|
|
64
64
|
function workspaceRoot() {
|
|
65
|
-
|
|
65
|
+
const activeUri = vscode2.window.activeTextEditor?.document.uri;
|
|
66
|
+
return (activeUri ? vscode2.workspace.getWorkspaceFolder(activeUri) : void 0)?.uri.fsPath ?? vscode2.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
66
67
|
}
|
|
67
68
|
function diffsRoot(root) {
|
|
68
69
|
return path.join(root, ".ur", "ide", "diffs");
|
|
@@ -71,10 +72,34 @@ function manifestPath(root) {
|
|
|
71
72
|
return path.join(diffsRoot(root), "manifest.json");
|
|
72
73
|
}
|
|
73
74
|
function patchPath(root, bundle) {
|
|
74
|
-
return
|
|
75
|
+
return artifactPath(root, bundle, "patch");
|
|
75
76
|
}
|
|
76
77
|
function metadataPath(root, bundle) {
|
|
77
|
-
return
|
|
78
|
+
return artifactPath(root, bundle, "metadata");
|
|
79
|
+
}
|
|
80
|
+
var DIFF_ID_PATTERN = /^diff-[1-9][0-9]*$/u;
|
|
81
|
+
function artifactPath(root, bundle, kind) {
|
|
82
|
+
if (!DIFF_ID_PATTERN.test(bundle.id)) throw new Error(`Invalid UR diff id: ${bundle.id}`);
|
|
83
|
+
const relative2 = kind === "patch" ? bundle.patchFile : bundle.metadataFile;
|
|
84
|
+
const expected = kind === "patch" ? `patches/${bundle.id}.patch` : `metadata/${bundle.id}.json`;
|
|
85
|
+
if (relative2.replaceAll("\\", "/") !== expected) {
|
|
86
|
+
throw new Error(`Invalid UR diff ${kind} path for ${bundle.id}`);
|
|
87
|
+
}
|
|
88
|
+
const rootPath = path.resolve(diffsRoot(root));
|
|
89
|
+
const target = path.resolve(rootPath, relative2);
|
|
90
|
+
if (!target.startsWith(`${rootPath}${path.sep}`)) throw new Error(`UR diff ${kind} path escapes the diff store`);
|
|
91
|
+
return target;
|
|
92
|
+
}
|
|
93
|
+
function isValidBundle(value) {
|
|
94
|
+
if (!value || typeof value !== "object") return false;
|
|
95
|
+
const bundle = value;
|
|
96
|
+
try {
|
|
97
|
+
patchPath(".", bundle);
|
|
98
|
+
metadataPath(".", bundle);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
78
103
|
}
|
|
79
104
|
function readJson(file, fallback) {
|
|
80
105
|
try {
|
|
@@ -90,10 +115,11 @@ function writeJson(file, value) {
|
|
|
90
115
|
}
|
|
91
116
|
function loadManifest(root) {
|
|
92
117
|
const manifest = readJson(manifestPath(root), { version: 1, diffs: [] });
|
|
93
|
-
return Array.isArray(manifest.diffs) ? manifest : { version: 1, diffs: [] };
|
|
118
|
+
return Array.isArray(manifest.diffs) ? { version: 1, diffs: manifest.diffs.filter(isValidBundle) } : { version: 1, diffs: [] };
|
|
94
119
|
}
|
|
95
120
|
function loadBundleMetadata(root, bundle) {
|
|
96
|
-
|
|
121
|
+
const metadata = readJson(metadataPath(root, bundle), bundle);
|
|
122
|
+
return isValidBundle(metadata) && metadata.id === bundle.id ? metadata : bundle;
|
|
97
123
|
}
|
|
98
124
|
function readPatch(root, bundle) {
|
|
99
125
|
const file = patchPath(root, bundle);
|
|
@@ -720,9 +746,9 @@ function languageIdToFence(languageId) {
|
|
|
720
746
|
}
|
|
721
747
|
function captureEditorSnapshot() {
|
|
722
748
|
const vscode18 = require("vscode");
|
|
723
|
-
const workspaceRoot2 = vscode18.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
724
749
|
const editor = vscode18.window.activeTextEditor;
|
|
725
|
-
if (!editor) return { workspaceRoot:
|
|
750
|
+
if (!editor) return { workspaceRoot: vscode18.workspace.workspaceFolders?.[0]?.uri.fsPath };
|
|
751
|
+
const workspaceRoot2 = vscode18.workspace.getWorkspaceFolder(editor.document.uri)?.uri.fsPath;
|
|
726
752
|
const absolutePath = editor.document.uri.fsPath;
|
|
727
753
|
const relativePath = workspaceRoot2 ? path2.relative(workspaceRoot2, absolutePath) : absolutePath;
|
|
728
754
|
const activeFile = { path: relativePath, languageId: editor.document.languageId };
|
|
@@ -1294,12 +1320,16 @@ var ChatController = class {
|
|
|
1294
1320
|
attachments = [];
|
|
1295
1321
|
status = "idle";
|
|
1296
1322
|
turnHandle;
|
|
1323
|
+
activeTurnId = 0;
|
|
1297
1324
|
pendingPermissions = /* @__PURE__ */ new Map();
|
|
1298
1325
|
// --- commands ---
|
|
1299
1326
|
async newChat() {
|
|
1300
1327
|
const root = this.requireWorkspaceRoot();
|
|
1301
1328
|
if (!root) return;
|
|
1302
1329
|
this.turnHandle?.cancel();
|
|
1330
|
+
this.activeTurnId++;
|
|
1331
|
+
this.turnHandle = void 0;
|
|
1332
|
+
this.denyAllPending("A new chat was started.");
|
|
1303
1333
|
this.record = createSession(root);
|
|
1304
1334
|
this.attachments = [];
|
|
1305
1335
|
this.status = "idle";
|
|
@@ -1418,6 +1448,10 @@ var ChatController = class {
|
|
|
1418
1448
|
vscode6.window.showWarningMessage(reason);
|
|
1419
1449
|
return;
|
|
1420
1450
|
}
|
|
1451
|
+
if (this.record && snapshot.workspaceRoot !== this.record.session.workspaceRoot) {
|
|
1452
|
+
vscode6.window.showWarningMessage("The active file belongs to a different workspace folder. Start a new chat for that folder.");
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1421
1455
|
const attachment = kind === "file" ? { kind: "file", file: snapshot.activeFile } : { kind: "selection", selection: snapshot.selection };
|
|
1422
1456
|
this.attachments.push(attachment);
|
|
1423
1457
|
this.ensurePanel();
|
|
@@ -1437,7 +1471,7 @@ var ChatController = class {
|
|
|
1437
1471
|
/** The single pathway every turn goes through — manual sends and editor
|
|
1438
1472
|
* actions alike. */
|
|
1439
1473
|
async dispatchTurn(promptText) {
|
|
1440
|
-
const root = this.requireWorkspaceRoot();
|
|
1474
|
+
const root = this.record?.session.workspaceRoot ?? this.requireWorkspaceRoot();
|
|
1441
1475
|
if (!root) return;
|
|
1442
1476
|
if (this.status === "running") {
|
|
1443
1477
|
vscode6.window.showWarningMessage("UR is already running a request. Cancel it first or wait for it to finish.");
|
|
@@ -1460,17 +1494,24 @@ var ChatController = class {
|
|
|
1460
1494
|
this._onDidChangeState.fire();
|
|
1461
1495
|
this.panel?.post({ type: "statusChanged", status: this.status });
|
|
1462
1496
|
const resumeSessionId = this.record.session.cliSessionId;
|
|
1463
|
-
|
|
1497
|
+
const turnId = ++this.activeTurnId;
|
|
1498
|
+
let exitedSynchronously = false;
|
|
1499
|
+
const handle = runUrTurn(
|
|
1464
1500
|
{ cwd: root, prompt: promptText, resumeSessionId },
|
|
1465
1501
|
{
|
|
1466
|
-
onMessage: (message) => this.handleStreamMessage(root, sessionId, message),
|
|
1467
|
-
onControlRequest: (request) => this.handlePermissionRequest(request),
|
|
1468
|
-
onExit: (result) =>
|
|
1502
|
+
onMessage: (message) => this.handleStreamMessage(root, sessionId, turnId, message),
|
|
1503
|
+
onControlRequest: (request) => this.handlePermissionRequest(turnId, request),
|
|
1504
|
+
onExit: (result) => {
|
|
1505
|
+
exitedSynchronously = true;
|
|
1506
|
+
this.handleTurnExit(root, sessionId, turnId, result);
|
|
1507
|
+
}
|
|
1469
1508
|
},
|
|
1470
1509
|
{ executable: resolveUrCommand({ cwd: root, config: readUrCommandConfig() }) }
|
|
1471
1510
|
);
|
|
1511
|
+
this.turnHandle = exitedSynchronously ? void 0 : handle;
|
|
1472
1512
|
}
|
|
1473
|
-
handleStreamMessage(root, sessionId, message) {
|
|
1513
|
+
handleStreamMessage(root, sessionId, turnId, message) {
|
|
1514
|
+
if (turnId !== this.activeTurnId) return;
|
|
1474
1515
|
if (message.type === "system" && message.subtype === "init" && typeof message.session_id === "string") {
|
|
1475
1516
|
if (this.record && this.record.session.id === sessionId && !this.record.session.cliSessionId) {
|
|
1476
1517
|
setCliSessionId(root, sessionId, message.session_id);
|
|
@@ -1508,24 +1549,28 @@ var ChatController = class {
|
|
|
1508
1549
|
if (!this.record) return;
|
|
1509
1550
|
this.appendChatMessage(root, this.record.session.id, "status", [{ type: "text", text }]);
|
|
1510
1551
|
}
|
|
1511
|
-
handlePermissionRequest(request) {
|
|
1512
|
-
|
|
1552
|
+
handlePermissionRequest(turnId, request) {
|
|
1553
|
+
if (turnId !== this.activeTurnId) {
|
|
1554
|
+
return Promise.resolve({ behavior: "deny", message: "This chat turn is no longer active." });
|
|
1555
|
+
}
|
|
1556
|
+
return new Promise((resolve3) => {
|
|
1513
1557
|
const toolName = request.request.tool_name ?? "tool";
|
|
1514
1558
|
const input = request.request.input ?? {};
|
|
1515
|
-
this.pendingPermissions.set(request.request_id, { resolve:
|
|
1559
|
+
this.pendingPermissions.set(request.request_id, { resolve: resolve3, toolName, input, turnId });
|
|
1516
1560
|
this.panel?.post({ type: "permissionRequest", requestId: request.request_id, toolName, input });
|
|
1517
1561
|
});
|
|
1518
1562
|
}
|
|
1519
|
-
handleTurnExit(root, result) {
|
|
1563
|
+
handleTurnExit(root, sessionId, turnId, result) {
|
|
1564
|
+
if (turnId !== this.activeTurnId) return;
|
|
1520
1565
|
this.turnHandle = void 0;
|
|
1521
1566
|
this.denyAllPending("The chat turn ended before this request was answered.");
|
|
1522
1567
|
if (result.canceled) {
|
|
1523
1568
|
this.status = "canceled";
|
|
1524
|
-
this.
|
|
1569
|
+
this.appendChatMessage(root, sessionId, "status", [{ type: "text", text: "Canceled." }]);
|
|
1525
1570
|
} else if (!result.ok) {
|
|
1526
1571
|
this.status = "error";
|
|
1527
1572
|
const message = result.error ?? "UR failed to complete this turn.";
|
|
1528
|
-
this.
|
|
1573
|
+
this.appendChatMessage(root, sessionId, "status", [{ type: "text", text: `Error: ${message}` }]);
|
|
1529
1574
|
this.panel?.post({ type: "errorBanner", message });
|
|
1530
1575
|
} else {
|
|
1531
1576
|
this.status = "idle";
|
|
@@ -1535,13 +1580,13 @@ var ChatController = class {
|
|
|
1535
1580
|
}
|
|
1536
1581
|
resolvePermission(requestId, decision) {
|
|
1537
1582
|
const pending = this.pendingPermissions.get(requestId);
|
|
1538
|
-
if (!pending) return;
|
|
1583
|
+
if (!pending || pending.turnId !== this.activeTurnId) return;
|
|
1539
1584
|
this.pendingPermissions.delete(requestId);
|
|
1540
1585
|
pending.resolve(
|
|
1541
1586
|
decision === "allow" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "User denied this tool call from the UR Chat panel." }
|
|
1542
1587
|
);
|
|
1543
1588
|
this.panel?.post({ type: "permissionResolved", requestId });
|
|
1544
|
-
const root = workspaceRoot
|
|
1589
|
+
const root = this.record?.session.workspaceRoot;
|
|
1545
1590
|
if (root) this.appendStatusText(root, `${decision === "allow" ? "Allowed" : "Denied"} ${pending.toolName}.`);
|
|
1546
1591
|
}
|
|
1547
1592
|
denyAllPending(reason) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "ur-inline-diffs",
|
|
3
3
|
"displayName": "UR Inline Diffs",
|
|
4
4
|
"description": "Review, apply, and reject UR inline diff bundles from .ur/ide/diffs inside VS Code.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.45.2",
|
|
6
6
|
"publisher": "ur-nexus",
|
|
7
7
|
"engines": {
|
|
8
8
|
"vscode": "^1.92.0"
|