@nghyane/arcane 0.1.11 → 0.1.12
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 +11 -0
- package/package.json +3 -3
- package/src/extensibility/plugins/installer.ts +6 -21
- package/src/extensibility/plugins/manager.ts +8 -29
- package/src/modes/components/welcome.ts +8 -9
- package/src/modes/controllers/event-controller.ts +51 -12
- package/src/patch/index.ts +1 -1
- package/src/stt/downloader.ts +1 -4
- package/src/stt/setup.ts +2 -4
- package/src/utils/open.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.12] - 2026-02-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserve single blank line content in edit tool — `hashlineParseContent` no longer strips the only line when it is empty
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Stream codemode intent immediately during LLM generation instead of waiting for execution start
|
|
14
|
+
- Hide loader spinner when codemode group is active to avoid duplicate status indicators
|
|
15
|
+
|
|
5
16
|
## [0.1.8] - 2026-02-22
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@nghyane/arcane",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.12",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/nghyane/arcane",
|
|
7
7
|
"author": "Can Bölük",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@mozilla/readability": "0.6.0",
|
|
47
47
|
"@nghyane/arcane-stats": "^0.1.8",
|
|
48
|
-
"@nghyane/arcane-agent": "^0.1.
|
|
49
|
-
"@nghyane/arcane-codemode": "^0.1.
|
|
48
|
+
"@nghyane/arcane-agent": "^0.1.10",
|
|
49
|
+
"@nghyane/arcane-codemode": "^0.1.11",
|
|
50
50
|
"@nghyane/arcane-ai": "^0.1.8",
|
|
51
51
|
"@nghyane/arcane-natives": "^0.1.7",
|
|
52
52
|
"@nghyane/arcane-tui": "^0.1.9",
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isEnoent } from "@nghyane/arcane-utils";
|
|
4
4
|
import { getAgentDir, getProjectDir } from "@nghyane/arcane-utils/dirs";
|
|
5
|
+
import { $ } from "bun";
|
|
5
6
|
import type { InstalledPlugin } from "./types";
|
|
6
7
|
|
|
7
8
|
const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
|
|
@@ -45,17 +46,9 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// Run npm install in plugins directory
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
stdout: "pipe",
|
|
52
|
-
stderr: "pipe",
|
|
53
|
-
windowsHide: true,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const exitCode = await proc.exited;
|
|
57
|
-
if (exitCode !== 0) {
|
|
58
|
-
const stderr = await new Response(proc.stderr).text();
|
|
49
|
+
const result = await $`bun install ${packageName}`.cwd(PLUGINS_DIR).quiet().nothrow();
|
|
50
|
+
if (result.exitCode !== 0) {
|
|
51
|
+
const stderr = result.stderr.toString().trim();
|
|
59
52
|
throw new Error(`Failed to install ${packageName}: ${stderr}`);
|
|
60
53
|
}
|
|
61
54
|
|
|
@@ -87,16 +80,8 @@ export async function uninstallPlugin(name: string): Promise<void> {
|
|
|
87
80
|
|
|
88
81
|
await ensurePluginsDir();
|
|
89
82
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
stdin: "ignore",
|
|
93
|
-
stdout: "pipe",
|
|
94
|
-
stderr: "pipe",
|
|
95
|
-
windowsHide: true,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const exitCode = await proc.exited;
|
|
99
|
-
if (exitCode !== 0) {
|
|
83
|
+
const result = await $`bun uninstall ${name}`.cwd(PLUGINS_DIR).quiet().nothrow();
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
100
85
|
throw new Error(`Failed to uninstall ${name}`);
|
|
101
86
|
}
|
|
102
87
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getProjectDir,
|
|
10
10
|
getProjectPluginOverridesPath,
|
|
11
11
|
} from "@nghyane/arcane-utils/dirs";
|
|
12
|
+
import { $ } from "bun";
|
|
12
13
|
import { extractPackageName, parsePluginSpec } from "./parser";
|
|
13
14
|
import type {
|
|
14
15
|
DoctorCheck,
|
|
@@ -155,17 +156,9 @@ export class PluginManager {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Run npm install
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
stdout: "pipe",
|
|
162
|
-
stderr: "pipe",
|
|
163
|
-
windowsHide: true,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const exitCode = await proc.exited;
|
|
167
|
-
if (exitCode !== 0) {
|
|
168
|
-
const stderr = await new Response(proc.stderr).text();
|
|
159
|
+
const result = await $`bun install ${spec.packageName}`.cwd(getPluginsDir()).quiet().nothrow();
|
|
160
|
+
if (result.exitCode !== 0) {
|
|
161
|
+
const stderr = result.stderr.toString().trim();
|
|
169
162
|
throw new Error(`npm install failed: ${stderr}`);
|
|
170
163
|
}
|
|
171
164
|
|
|
@@ -236,16 +229,8 @@ export class PluginManager {
|
|
|
236
229
|
validatePackageName(name);
|
|
237
230
|
await this.#ensurePackageJson();
|
|
238
231
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
stdin: "ignore",
|
|
242
|
-
stdout: "pipe",
|
|
243
|
-
stderr: "pipe",
|
|
244
|
-
windowsHide: true,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const exitCode = await proc.exited;
|
|
248
|
-
if (exitCode !== 0) {
|
|
232
|
+
const result = await $`bun uninstall ${name}`.cwd(getPluginsDir()).quiet().nothrow();
|
|
233
|
+
if (result.exitCode !== 0) {
|
|
249
234
|
throw new Error(`npm uninstall failed for ${name}`);
|
|
250
235
|
}
|
|
251
236
|
|
|
@@ -619,14 +604,8 @@ export class PluginManager {
|
|
|
619
604
|
|
|
620
605
|
async #fixMissingPlugin(): Promise<boolean> {
|
|
621
606
|
try {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
stdin: "ignore",
|
|
625
|
-
stdout: "pipe",
|
|
626
|
-
stderr: "pipe",
|
|
627
|
-
windowsHide: true,
|
|
628
|
-
});
|
|
629
|
-
return (await proc.exited) === 0;
|
|
607
|
+
const result = await $`bun install`.cwd(getPluginsDir()).quiet().nothrow();
|
|
608
|
+
return result.exitCode === 0;
|
|
630
609
|
} catch {
|
|
631
610
|
return false;
|
|
632
611
|
}
|
|
@@ -67,9 +67,9 @@ export class WelcomeComponent implements Component {
|
|
|
67
67
|
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
68
68
|
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
69
69
|
|
|
70
|
-
// Block-based
|
|
70
|
+
// Block-based ARC logo (gradient: blue → cyan → green / Nord Frost)
|
|
71
71
|
// biome-ignore format: preserve ASCII art layout
|
|
72
|
-
const piLogo = ["
|
|
72
|
+
const piLogo = ["╭━━━╮╭━━━╮╭━━━╮", "┃╭━╮┃┃╭━╮┃┃╭━━╯", "┃╰━╯┃┃╰━╯┃┃┃ ", "┃┃ ┃┃┃╭╮╭╯┃╰━━╮", "╰╯ ╰╯╰╯╰╯ ╰━━━╯"];
|
|
73
73
|
|
|
74
74
|
// Apply gradient to logo
|
|
75
75
|
const logoColored = piLogo.map(line => this.#gradientLine(line));
|
|
@@ -190,15 +190,14 @@ export class WelcomeComponent implements Component {
|
|
|
190
190
|
return padding(leftPad) + text + padding(rightPad);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/** Apply
|
|
193
|
+
/** Apply Nord Frost gradient (blue → cyan → green) to a string */
|
|
194
194
|
#gradientLine(line: string): string {
|
|
195
195
|
const colors = [
|
|
196
|
-
"\x1b[38;
|
|
197
|
-
"\x1b[38;
|
|
198
|
-
"\x1b[38;
|
|
199
|
-
"\x1b[38;
|
|
200
|
-
"\x1b[38;
|
|
201
|
-
"\x1b[38;5;51m", // bright cyan
|
|
196
|
+
"\x1b[38;2;136;192;208m", // #88c0d0 blue
|
|
197
|
+
"\x1b[38;2;141;200;200m", // blend
|
|
198
|
+
"\x1b[38;2;143;188;187m", // #8fbcbb cyan
|
|
199
|
+
"\x1b[38;2;153;189;170m", // blend
|
|
200
|
+
"\x1b[38;2;163;190;140m", // #a3be8c green
|
|
202
201
|
];
|
|
203
202
|
const reset = "\x1b[0m";
|
|
204
203
|
|
|
@@ -42,6 +42,39 @@ export class EventController {
|
|
|
42
42
|
this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
#ensureCodemodeGroup(id: string): CodeModeGroupComponent {
|
|
46
|
+
let group = this.#codemodeGroups.get(id);
|
|
47
|
+
if (!group) {
|
|
48
|
+
this.#resetReadGroup();
|
|
49
|
+
group = new CodeModeGroupComponent(this.ctx.ui);
|
|
50
|
+
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
51
|
+
this.ctx.chatContainer.addChild(group);
|
|
52
|
+
this.#codemodeGroups.set(id, group);
|
|
53
|
+
this.ctx.pendingTools.set(id, group);
|
|
54
|
+
this.#hideLoader();
|
|
55
|
+
}
|
|
56
|
+
return group;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#hideLoader(): void {
|
|
60
|
+
if (!this.ctx.loadingAnimation) return;
|
|
61
|
+
this.ctx.loadingAnimation.stop();
|
|
62
|
+
this.ctx.statusContainer.clear();
|
|
63
|
+
this.ctx.loadingAnimation = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#restoreLoader(): void {
|
|
67
|
+
if (this.ctx.loadingAnimation || this.#codemodeGroups.size > 0) return;
|
|
68
|
+
this.ctx.loadingAnimation = new Loader(
|
|
69
|
+
this.ctx.ui,
|
|
70
|
+
spinner => theme.fg("accent", spinner),
|
|
71
|
+
text => theme.fg("muted", text),
|
|
72
|
+
`Working\u2026 (esc to interrupt)`,
|
|
73
|
+
getSymbolTheme().spinnerFrames,
|
|
74
|
+
);
|
|
75
|
+
this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
subscribeToAgent(): void {
|
|
46
79
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
47
80
|
await this.handleEvent(event);
|
|
@@ -132,8 +165,16 @@ export class EventController {
|
|
|
132
165
|
|
|
133
166
|
for (const content of this.ctx.streamingMessage.content) {
|
|
134
167
|
if (content.type !== "toolCall") continue;
|
|
135
|
-
// Code Mode:
|
|
136
|
-
if (content.name === "code")
|
|
168
|
+
// Code Mode: create group component early during streaming for intent display
|
|
169
|
+
if (content.name === "code") {
|
|
170
|
+
const group = this.#ensureCodemodeGroup(content.id);
|
|
171
|
+
const args = content.arguments;
|
|
172
|
+
if (args && typeof args === "object" && INTENT_FIELD in args) {
|
|
173
|
+
const intent = (args[INTENT_FIELD] as string | undefined)?.trim();
|
|
174
|
+
if (intent) group.setIntent(intent);
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
137
178
|
|
|
138
179
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
139
180
|
if (content.name === "read") {
|
|
@@ -169,9 +210,10 @@ export class EventController {
|
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
|
|
172
|
-
// Update working message with intent
|
|
213
|
+
// Update working message with intent — skip for code tools that already have a visible group
|
|
173
214
|
for (const content of this.ctx.streamingMessage.content) {
|
|
174
215
|
if (content.type !== "toolCall") continue;
|
|
216
|
+
if (this.#codemodeGroups.has(content.id)) continue;
|
|
175
217
|
const args = content.arguments;
|
|
176
218
|
if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
|
|
177
219
|
this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
|
|
@@ -218,19 +260,15 @@ export class EventController {
|
|
|
218
260
|
break;
|
|
219
261
|
|
|
220
262
|
case "tool_execution_start": {
|
|
221
|
-
this.#updateWorkingMessageFromIntent(event.intent);
|
|
222
|
-
// Code Mode: create a group component for the "code" tool
|
|
263
|
+
if (!this.#codemodeGroups.has(event.toolCallId)) this.#updateWorkingMessageFromIntent(event.intent);
|
|
223
264
|
if (event.toolName === "code") {
|
|
224
|
-
this.#
|
|
225
|
-
const
|
|
226
|
-
|
|
265
|
+
const group = this.#ensureCodemodeGroup(event.toolCallId);
|
|
266
|
+
const intent = (event.intent ?? (event.args as Record<string, unknown>)?.agent__intent) as
|
|
267
|
+
| string
|
|
268
|
+
| undefined;
|
|
227
269
|
if (typeof intent === "string" && intent.trim()) {
|
|
228
270
|
group.setIntent(intent.trim());
|
|
229
271
|
}
|
|
230
|
-
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
231
|
-
this.ctx.chatContainer.addChild(group);
|
|
232
|
-
this.#codemodeGroups.set(event.toolCallId, group);
|
|
233
|
-
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
234
272
|
this.ctx.ui.requestRender();
|
|
235
273
|
break;
|
|
236
274
|
}
|
|
@@ -316,6 +354,7 @@ export class EventController {
|
|
|
316
354
|
group.setDone();
|
|
317
355
|
this.#codemodeGroups.delete(event.toolCallId);
|
|
318
356
|
}
|
|
357
|
+
this.#restoreLoader();
|
|
319
358
|
}
|
|
320
359
|
// Update todo display when todo_write tool completes
|
|
321
360
|
if (event.toolName === "todo_write" && !event.isError) {
|
package/src/patch/index.ts
CHANGED
|
@@ -199,7 +199,7 @@ function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
199
199
|
if (Array.isArray(edit)) return edit;
|
|
200
200
|
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
201
201
|
if (lines.length === 0) return [];
|
|
202
|
-
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
202
|
+
if (lines.length > 1 && lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
203
203
|
return lines;
|
|
204
204
|
}
|
|
205
205
|
|
package/src/stt/downloader.ts
CHANGED
|
@@ -46,10 +46,7 @@ async function ensurePythonWhisper(options?: EnsureOptions): Promise<void> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Check if whisper module is already importable
|
|
49
|
-
const check =
|
|
50
|
-
stdout: "pipe",
|
|
51
|
-
stderr: "pipe",
|
|
52
|
-
});
|
|
49
|
+
const check = await $`${pythonCmd} -c ${"import whisper"}`.quiet().nothrow();
|
|
53
50
|
if (check.exitCode === 0) return;
|
|
54
51
|
|
|
55
52
|
options?.onProgress?.({ stage: "Installing openai-whisper (this may take a few minutes)..." });
|
package/src/stt/setup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
1
2
|
import { detectRecordingTools } from "./recorder";
|
|
2
3
|
import { resolvePython } from "./transcriber";
|
|
3
4
|
|
|
@@ -20,10 +21,7 @@ export async function checkDependencies(): Promise<STTDependencyStatus> {
|
|
|
20
21
|
|
|
21
22
|
let whisperAvailable = false;
|
|
22
23
|
if (pythonCmd) {
|
|
23
|
-
const check =
|
|
24
|
-
stdout: "pipe",
|
|
25
|
-
stderr: "pipe",
|
|
26
|
-
});
|
|
24
|
+
const check = await $`${pythonCmd} -c ${"import whisper"}`.quiet().nothrow();
|
|
27
25
|
whisperAvailable = check.exitCode === 0;
|
|
28
26
|
}
|
|
29
27
|
const whisperHint = "Run 'arc setup stt' to auto-install, or: pip install openai-whisper";
|
package/src/utils/open.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
1
2
|
/** Open a URL or file path in the default browser/application. Best-effort, never throws. */
|
|
2
3
|
export function openPath(urlOrPath: string): void {
|
|
3
4
|
let cmd: string[];
|
|
@@ -13,7 +14,7 @@ export function openPath(urlOrPath: string): void {
|
|
|
13
14
|
break;
|
|
14
15
|
}
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
+
$`${cmd}`.quiet().nothrow();
|
|
17
18
|
} catch {
|
|
18
19
|
// Best-effort: browser opening is non-critical
|
|
19
20
|
}
|