@nghyane/arcane 0.1.17 → 0.1.18
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/package.json +6 -6
- package/src/cli/setup-cli.ts +2 -62
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +1 -4
- package/src/config/settings-schema.ts +4 -52
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/hooks/tool-wrapper.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -2
- package/src/modes/controllers/command-controller.ts +0 -2
- package/src/modes/controllers/input-controller.ts +123 -6
- package/src/modes/interactive-mode.ts +1 -84
- package/src/modes/types.ts +0 -1
- package/src/patch/edit-tool.ts +2 -11
- package/src/prompts/agents/explore.md +4 -2
- package/src/prompts/agents/librarian.md +4 -6
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +5 -1
- package/src/prompts/system/system-prompt.md +15 -8
- package/src/sdk.ts +11 -18
- package/src/session/agent-session.ts +1 -7
- package/src/session/session-manager.ts +0 -30
- package/src/session/streaming-edit.ts +1 -36
- package/src/tools/bash.ts +2 -1
- package/src/tools/create-tools.ts +2 -33
- package/src/tools/fetch.ts +1 -1
- package/src/tools/grep.ts +2 -1
- package/src/tools/python.ts +53 -1
- package/src/tools/read.ts +2 -1
- package/src/tools/write.ts +1 -1
- package/src/web/search/index.ts +2 -1
- package/src/patch/normative.ts +0 -72
- package/src/stt/downloader.ts +0 -68
- package/src/stt/index.ts +0 -3
- package/src/stt/recorder.ts +0 -351
- package/src/stt/setup.ts +0 -50
- package/src/stt/stt-controller.ts +0 -160
- package/src/stt/transcribe.py +0 -70
- package/src/stt/transcriber.ts +0 -91
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.18",
|
|
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",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@mozilla/readability": "0.6.0",
|
|
47
|
-
"@nghyane/arcane-stats": "^0.1.
|
|
48
|
-
"@nghyane/arcane-agent": "^0.1.
|
|
49
|
-
"@nghyane/arcane-ai": "^0.1.
|
|
50
|
-
"@nghyane/arcane-natives": "^0.1.
|
|
51
|
-
"@nghyane/arcane-tui": "^0.1.
|
|
47
|
+
"@nghyane/arcane-stats": "^0.1.11",
|
|
48
|
+
"@nghyane/arcane-agent": "^0.1.14",
|
|
49
|
+
"@nghyane/arcane-ai": "^0.1.11",
|
|
50
|
+
"@nghyane/arcane-natives": "^0.1.9",
|
|
51
|
+
"@nghyane/arcane-tui": "^0.1.13",
|
|
52
52
|
"@nghyane/arcane-utils": "^0.1.7",
|
|
53
53
|
"@sinclair/typebox": "^0.34.48",
|
|
54
54
|
"@xterm/headless": "^6.0.0",
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { $ } from "bun";
|
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { theme } from "../theme/theme";
|
|
11
11
|
|
|
12
|
-
export type SetupComponent = "python"
|
|
12
|
+
export type SetupComponent = "python";
|
|
13
13
|
|
|
14
14
|
export interface SetupCommandArgs {
|
|
15
15
|
component: SetupComponent;
|
|
@@ -19,7 +19,7 @@ export interface SetupCommandArgs {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const VALID_COMPONENTS: SetupComponent[] = ["python"
|
|
22
|
+
const VALID_COMPONENTS: SetupComponent[] = ["python"];
|
|
23
23
|
|
|
24
24
|
const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
|
|
25
25
|
const MANAGED_PYTHON_ENV = getPythonEnvDir();
|
|
@@ -206,9 +206,6 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
206
206
|
case "python":
|
|
207
207
|
await handlePythonSetup(cmd.flags);
|
|
208
208
|
break;
|
|
209
|
-
case "stt":
|
|
210
|
-
await handleSttSetup(cmd.flags);
|
|
211
|
-
break;
|
|
212
209
|
}
|
|
213
210
|
}
|
|
214
211
|
|
|
@@ -295,60 +292,6 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
|
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
294
|
|
|
298
|
-
async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
|
|
299
|
-
const { checkDependencies, formatDependencyStatus } = await import("../stt/setup");
|
|
300
|
-
const status = await checkDependencies();
|
|
301
|
-
|
|
302
|
-
if (flags.json) {
|
|
303
|
-
console.log(JSON.stringify(status, null, 2));
|
|
304
|
-
if (!status.recorder.available || !status.python.available || !status.whisper.available) process.exit(1);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
console.log(formatDependencyStatus(status));
|
|
309
|
-
|
|
310
|
-
if (status.recorder.available && status.python.available && status.whisper.available) {
|
|
311
|
-
console.log(chalk.green(`\n${theme.status.success} Speech-to-text is ready`));
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (flags.check) {
|
|
316
|
-
process.exit(1);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!status.python.available) {
|
|
320
|
-
console.error(chalk.red(`\n${theme.status.error} Python not found`));
|
|
321
|
-
console.error(chalk.dim("Install Python 3.8+ and ensure it's in your PATH"));
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!status.recorder.available) {
|
|
326
|
-
console.error(chalk.yellow(`\n${theme.status.warning} No recording tool found`));
|
|
327
|
-
console.error(chalk.dim(status.recorder.installHint));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (!status.whisper.available) {
|
|
331
|
-
console.log(chalk.dim(`\nInstalling openai-whisper...`));
|
|
332
|
-
const { resolvePython } = await import("../stt/transcriber");
|
|
333
|
-
const pythonCmd = resolvePython()!;
|
|
334
|
-
const result = await $`${pythonCmd} -m pip install -q openai-whisper`.nothrow();
|
|
335
|
-
if (result.exitCode !== 0) {
|
|
336
|
-
console.error(chalk.red(`\n${theme.status.error} Failed to install openai-whisper`));
|
|
337
|
-
console.error(chalk.dim("Try manually: pip install openai-whisper"));
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const recheck = await checkDependencies();
|
|
343
|
-
if (recheck.recorder.available && recheck.python.available && recheck.whisper.available) {
|
|
344
|
-
console.log(chalk.green(`\n${theme.status.success} Speech-to-text is ready`));
|
|
345
|
-
} else {
|
|
346
|
-
console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
|
|
347
|
-
console.log(formatDependencyStatus(recheck));
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
295
|
/**
|
|
353
296
|
* Print setup command help.
|
|
354
297
|
*/
|
|
@@ -360,7 +303,6 @@ ${chalk.bold("Usage:")}
|
|
|
360
303
|
|
|
361
304
|
${chalk.bold("Components:")}
|
|
362
305
|
python Install Jupyter kernel dependencies for Python code execution
|
|
363
|
-
stt Install speech-to-text dependencies (openai-whisper, recording tools)
|
|
364
306
|
Packages: ${PYTHON_PACKAGES.join(", ")}
|
|
365
307
|
|
|
366
308
|
${chalk.bold("Options:")}
|
|
@@ -369,8 +311,6 @@ ${chalk.bold("Options:")}
|
|
|
369
311
|
|
|
370
312
|
${chalk.bold("Examples:")}
|
|
371
313
|
${APP_NAME} setup python Install Python execution dependencies
|
|
372
|
-
${APP_NAME} setup stt Install speech-to-text dependencies
|
|
373
|
-
${APP_NAME} setup stt --check Check if STT dependencies are available
|
|
374
314
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
375
315
|
`);
|
|
376
316
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Args, Command, Flags, renderCommandHelp } from "@nghyane/arcane-utils/c
|
|
|
5
5
|
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
|
6
6
|
import { initTheme } from "../theme/theme";
|
|
7
7
|
|
|
8
|
-
const COMPONENTS: SetupComponent[] = ["python"
|
|
8
|
+
const COMPONENTS: SetupComponent[] = ["python"];
|
|
9
9
|
|
|
10
10
|
export default class Setup extends Command {
|
|
11
11
|
static description = "Install dependencies for optional features";
|
|
@@ -34,8 +34,7 @@ export type AppAction =
|
|
|
34
34
|
| "newSession"
|
|
35
35
|
| "tree"
|
|
36
36
|
| "fork"
|
|
37
|
-
| "resume"
|
|
38
|
-
| "toggleSTT";
|
|
37
|
+
| "resume";
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* All configurable actions.
|
|
@@ -73,7 +72,6 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
|
|
|
73
72
|
tree: [],
|
|
74
73
|
fork: [],
|
|
75
74
|
resume: [],
|
|
76
|
-
toggleSTT: "alt+h",
|
|
77
75
|
};
|
|
78
76
|
|
|
79
77
|
/**
|
|
@@ -106,7 +104,6 @@ const APP_ACTIONS: AppAction[] = [
|
|
|
106
104
|
"tree",
|
|
107
105
|
"fork",
|
|
108
106
|
"resume",
|
|
109
|
-
"toggleSTT",
|
|
110
107
|
];
|
|
111
108
|
|
|
112
109
|
function isAppAction(action: string): action is AppAction {
|
|
@@ -204,7 +204,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
204
204
|
steeringMode: {
|
|
205
205
|
type: "enum",
|
|
206
206
|
values: ["all", "one-at-a-time"] as const,
|
|
207
|
-
default: "
|
|
207
|
+
default: "all",
|
|
208
208
|
ui: {
|
|
209
209
|
tab: "agent",
|
|
210
210
|
label: "Steering mode",
|
|
@@ -214,7 +214,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
214
214
|
followUpMode: {
|
|
215
215
|
type: "enum",
|
|
216
216
|
values: ["all", "one-at-a-time"] as const,
|
|
217
|
-
default: "
|
|
217
|
+
default: "all",
|
|
218
218
|
ui: {
|
|
219
219
|
tab: "agent",
|
|
220
220
|
label: "Follow-up mode",
|
|
@@ -253,15 +253,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
253
253
|
submenu: true,
|
|
254
254
|
},
|
|
255
255
|
},
|
|
256
|
-
normativeRewrite: {
|
|
257
|
-
type: "boolean",
|
|
258
|
-
default: false,
|
|
259
|
-
ui: {
|
|
260
|
-
tab: "agent",
|
|
261
|
-
label: "Normative rewrite",
|
|
262
|
-
description: "Rewrite tool call arguments to normalized format in session history",
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
256
|
readLineNumbers: {
|
|
266
257
|
type: "boolean",
|
|
267
258
|
default: false,
|
|
@@ -586,7 +577,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
586
577
|
type: "boolean",
|
|
587
578
|
default: true,
|
|
588
579
|
ui: {
|
|
589
|
-
tab: "
|
|
580
|
+
tab: "input",
|
|
590
581
|
label: "Auto-resize images",
|
|
591
582
|
description: "Resize large images to 2000x2000 max for better model compatibility",
|
|
592
583
|
},
|
|
@@ -594,7 +585,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
594
585
|
"images.blockImages": {
|
|
595
586
|
type: "boolean",
|
|
596
587
|
default: false,
|
|
597
|
-
ui: { tab: "
|
|
588
|
+
ui: { tab: "input", label: "Block images", description: "Prevent images from being sent to LLM providers" },
|
|
598
589
|
},
|
|
599
590
|
|
|
600
591
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -796,36 +787,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
796
787
|
},
|
|
797
788
|
},
|
|
798
789
|
|
|
799
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
800
|
-
// STT settings
|
|
801
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
802
|
-
"stt.enabled": {
|
|
803
|
-
type: "boolean",
|
|
804
|
-
default: false,
|
|
805
|
-
ui: { tab: "input", label: "Speech-to-text", description: "Enable speech-to-text input via microphone" },
|
|
806
|
-
},
|
|
807
|
-
"stt.language": {
|
|
808
|
-
type: "string",
|
|
809
|
-
default: "en",
|
|
810
|
-
ui: {
|
|
811
|
-
tab: "input",
|
|
812
|
-
label: "STT language",
|
|
813
|
-
description: "Language code for transcription (e.g., en, es, fr)",
|
|
814
|
-
submenu: true,
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
"stt.modelName": {
|
|
818
|
-
type: "enum",
|
|
819
|
-
values: ["tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large"] as const,
|
|
820
|
-
default: "base.en",
|
|
821
|
-
ui: {
|
|
822
|
-
tab: "input",
|
|
823
|
-
label: "STT model",
|
|
824
|
-
description: "Whisper model size (larger = more accurate but slower)",
|
|
825
|
-
submenu: true,
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
|
|
829
790
|
// ─────────────────────────────────────────────────────────────────────────
|
|
830
791
|
// Edit settings
|
|
831
792
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1109,14 +1070,6 @@ export interface ThinkingBudgetsSettings {
|
|
|
1109
1070
|
high: number;
|
|
1110
1071
|
}
|
|
1111
1072
|
|
|
1112
|
-
export interface SttSettings {
|
|
1113
|
-
enabled: boolean;
|
|
1114
|
-
language: string | undefined;
|
|
1115
|
-
modelName: string;
|
|
1116
|
-
whisperPath: string | undefined;
|
|
1117
|
-
modelPath: string | undefined;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
1073
|
export interface BashInterceptorRule {
|
|
1121
1074
|
pattern: string;
|
|
1122
1075
|
flags?: string;
|
|
@@ -1136,7 +1089,6 @@ export interface GroupTypeMap {
|
|
|
1136
1089
|
exa: ExaSettings;
|
|
1137
1090
|
statusLine: StatusLineSettings;
|
|
1138
1091
|
thinkingBudgets: ThinkingBudgetsSettings;
|
|
1139
|
-
stt: SttSettings;
|
|
1140
1092
|
modelRoles: Record<string, string>;
|
|
1141
1093
|
}
|
|
1142
1094
|
|
|
@@ -178,10 +178,10 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
|
|
|
178
178
|
execute(
|
|
179
179
|
toolCallId: string,
|
|
180
180
|
params: Static<TParams>,
|
|
181
|
-
onUpdate: AgentToolUpdateCallback<TDetails
|
|
181
|
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
182
182
|
ctx: CustomToolContext,
|
|
183
183
|
signal?: AbortSignal,
|
|
184
|
-
): Promise<AgentToolResult<TDetails
|
|
184
|
+
): Promise<AgentToolResult<TDetails>>;
|
|
185
185
|
|
|
186
186
|
/** Called on session lifecycle events - use to reconstruct state or cleanup resources */
|
|
187
187
|
onSession?: (event: CustomToolSessionEvent, ctx: CustomToolContext) => void | Promise<void>;
|
|
@@ -26,7 +26,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
26
26
|
toolCallId: string,
|
|
27
27
|
params: Static<TParams>,
|
|
28
28
|
signal?: AbortSignal,
|
|
29
|
-
onUpdate?: AgentToolUpdateCallback<TDetails
|
|
29
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>,
|
|
30
30
|
context?: CustomToolContext,
|
|
31
31
|
) {
|
|
32
32
|
return this.tool.execute(toolCallId, params, onUpdate, context ?? this.getContext(), signal);
|
|
@@ -96,7 +96,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
96
96
|
toolCallId: string,
|
|
97
97
|
params: Static<TParameters>,
|
|
98
98
|
signal?: AbortSignal,
|
|
99
|
-
onUpdate?: AgentToolUpdateCallback<TDetails
|
|
99
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>,
|
|
100
100
|
context?: AgentToolContext,
|
|
101
101
|
) {
|
|
102
102
|
// Emit tool_call event - extensions can block execution
|
|
@@ -34,7 +34,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
|
|
|
34
34
|
toolCallId: string,
|
|
35
35
|
params: Static<TParameters>,
|
|
36
36
|
signal?: AbortSignal,
|
|
37
|
-
onUpdate?: AgentToolUpdateCallback<TDetails
|
|
37
|
+
onUpdate?: AgentToolUpdateCallback<TDetails>,
|
|
38
38
|
context?: AgentToolContext,
|
|
39
39
|
) {
|
|
40
40
|
// Emit tool_call event - hooks can block execution
|
|
@@ -59,9 +59,13 @@ export class CustomEditor extends Editor {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// Intercept Ctrl+V for image paste
|
|
62
|
+
// Intercept Ctrl+V for image paste — fall through to text paste if no image handled
|
|
63
63
|
if (matchesKey(data, "ctrl+v") && this.onCtrlV) {
|
|
64
|
-
void this.onCtrlV()
|
|
64
|
+
void this.onCtrlV().then(handled => {
|
|
65
|
+
if (!handled) {
|
|
66
|
+
super.handleInput(data);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -364,7 +364,6 @@ export class CommandController {
|
|
|
364
364
|
handleHotkeysCommand(): void {
|
|
365
365
|
const k = (id: string) => formatKeyHint(id as KeyId);
|
|
366
366
|
const expandToolsKey = this.ctx.keybindings.getDisplayString("expandTools") || k("ctrl+o");
|
|
367
|
-
const sttKey = this.ctx.keybindings.getDisplayString("toggleSTT") || k("alt+h");
|
|
368
367
|
const hotkeys = `
|
|
369
368
|
**Navigation**
|
|
370
369
|
| Key | Action |
|
|
@@ -400,7 +399,6 @@ export class CommandController {
|
|
|
400
399
|
| \`${expandToolsKey}\` | Toggle tool output expansion |
|
|
401
400
|
| \`${k("ctrl+t")}\` | Toggle todo list expansion |
|
|
402
401
|
| \`${k("ctrl+g")}\` | Edit message in external editor |
|
|
403
|
-
| \`${sttKey}\` | Toggle speech-to-text recording |
|
|
404
402
|
| \`/\` | Slash commands |
|
|
405
403
|
| \`!\` | Run bash command |
|
|
406
404
|
| \`!!\` | Run bash command (excluded from context) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import type { AgentMessage } from "@nghyane/arcane-agent";
|
|
3
|
-
import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@nghyane/arcane-natives";
|
|
3
|
+
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard, sanitizeText } from "@nghyane/arcane-natives";
|
|
4
4
|
import { $env } from "@nghyane/arcane-utils";
|
|
5
5
|
import { settings } from "../../config/settings";
|
|
6
6
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
@@ -10,8 +10,10 @@ import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registr
|
|
|
10
10
|
import { theme } from "../../theme/theme";
|
|
11
11
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
12
12
|
import { resizeImage } from "../../utils/image-resize";
|
|
13
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
13
14
|
import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
14
15
|
|
|
16
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
15
17
|
interface Expandable {
|
|
16
18
|
setExpanded(expanded: boolean): void;
|
|
17
19
|
}
|
|
@@ -123,9 +125,6 @@ export class InputController {
|
|
|
123
125
|
for (const key of this.ctx.keybindings.getKeys("followUp")) {
|
|
124
126
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.handleFollowUp());
|
|
125
127
|
}
|
|
126
|
-
for (const key of this.ctx.keybindings.getKeys("toggleSTT")) {
|
|
127
|
-
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
|
|
128
|
-
}
|
|
129
128
|
|
|
130
129
|
this.ctx.editor.onChange = (text: string) => {
|
|
131
130
|
const wasBashMode = this.ctx.isBashMode;
|
|
@@ -137,6 +136,9 @@ export class InputController {
|
|
|
137
136
|
this.ctx.updateEditorBorderColor();
|
|
138
137
|
}
|
|
139
138
|
};
|
|
139
|
+
|
|
140
|
+
this.ctx.editor.onPaste = (text: string) => this.handlePastedImagePaths(text);
|
|
141
|
+
this.ctx.editor.onHighlightLine = (text: string) => this.#highlightEditorLine(text);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
setupEditorSubmitHandler(): void {
|
|
@@ -478,6 +480,20 @@ export class InputController {
|
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
483
|
+
const sizeBytes = (imageData.data.length * 3) / 4;
|
|
484
|
+
if (sizeBytes > MAX_IMAGE_BYTES) {
|
|
485
|
+
try {
|
|
486
|
+
const compressed = await resizeImage(
|
|
487
|
+
{ type: "image", data: imageData.data, mimeType: imageData.mimeType },
|
|
488
|
+
{ maxBytes: MAX_IMAGE_BYTES },
|
|
489
|
+
);
|
|
490
|
+
imageData = { data: compressed.data, mimeType: compressed.mimeType };
|
|
491
|
+
} catch {
|
|
492
|
+
this.ctx.showStatus(`Image too large (${(sizeBytes / 1024 / 1024).toFixed(1)} MB, max 5 MB)`);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
481
497
|
this.ctx.pendingImages.push({
|
|
482
498
|
type: "image",
|
|
483
499
|
data: imageData.data,
|
|
@@ -490,8 +506,14 @@ export class InputController {
|
|
|
490
506
|
this.ctx.ui.requestRender();
|
|
491
507
|
return true;
|
|
492
508
|
}
|
|
493
|
-
|
|
494
|
-
|
|
509
|
+
|
|
510
|
+
// No image data — try text clipboard for file paths
|
|
511
|
+
const clipText = await readTextFromClipboard();
|
|
512
|
+
if (clipText && this.handlePastedImagePaths(clipText)) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Not an image — let caller fall through to normal text paste
|
|
495
517
|
return false;
|
|
496
518
|
} catch {
|
|
497
519
|
this.ctx.showStatus("Failed to read clipboard");
|
|
@@ -499,6 +521,101 @@ export class InputController {
|
|
|
499
521
|
}
|
|
500
522
|
}
|
|
501
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Detect image file paths in pasted text (e.g., drag-drop from file manager).
|
|
526
|
+
* Returns true if all pasted content was image paths (suppresses normal paste).
|
|
527
|
+
*/
|
|
528
|
+
handlePastedImagePaths(text: string): boolean {
|
|
529
|
+
const trimmed = text.trim();
|
|
530
|
+
if (!trimmed) return false;
|
|
531
|
+
|
|
532
|
+
// Split by newlines — drag-drop can paste multiple file paths
|
|
533
|
+
const lines = trimmed
|
|
534
|
+
.split(/\r?\n/)
|
|
535
|
+
.map(l => l.trim())
|
|
536
|
+
.filter(Boolean);
|
|
537
|
+
if (lines.length === 0 || lines.length > 20) return false;
|
|
538
|
+
|
|
539
|
+
// Check if ALL lines look like file paths to images
|
|
540
|
+
const IMAGE_EXT = /\.(png|jpe?g|gif|webp)$/i;
|
|
541
|
+
const paths: string[] = [];
|
|
542
|
+
for (const line of lines) {
|
|
543
|
+
// Terminals may quote or add file:// prefix
|
|
544
|
+
let p = line.replace(/^['"]|['"]$/g, "").replace(/^file:\/\//, "");
|
|
545
|
+
// Must look like an absolute or home-relative path with image extension
|
|
546
|
+
if (!/^[/~]/.test(p) || !IMAGE_EXT.test(p)) return false;
|
|
547
|
+
// Expand ~ to home dir
|
|
548
|
+
if (p.startsWith("~/")) {
|
|
549
|
+
p = `${process.env.HOME ?? ""}${p.slice(1)}`;
|
|
550
|
+
}
|
|
551
|
+
paths.push(p);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Process asynchronously, return true to suppress normal paste
|
|
555
|
+
void this.#loadImageFiles(paths);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async #loadImageFiles(paths: string[]): Promise<void> {
|
|
560
|
+
let loaded = 0;
|
|
561
|
+
for (const filePath of paths) {
|
|
562
|
+
try {
|
|
563
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(filePath);
|
|
564
|
+
if (!mimeType) {
|
|
565
|
+
this.ctx.showStatus(`Not a supported image: ${filePath}`);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const data = await Bun.file(filePath).bytes();
|
|
569
|
+
let base64Data = data.toBase64();
|
|
570
|
+
let finalMime = mimeType;
|
|
571
|
+
|
|
572
|
+
if (settings.get("images.autoResize")) {
|
|
573
|
+
try {
|
|
574
|
+
const resized = await resizeImage({ type: "image", data: base64Data, mimeType });
|
|
575
|
+
base64Data = resized.data;
|
|
576
|
+
finalMime = resized.mimeType;
|
|
577
|
+
} catch {
|
|
578
|
+
// Use original on resize failure
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const sizeBytes = (base64Data.length * 3) / 4;
|
|
583
|
+
if (sizeBytes > MAX_IMAGE_BYTES) {
|
|
584
|
+
try {
|
|
585
|
+
const compressed = await resizeImage(
|
|
586
|
+
{ type: "image", data: base64Data, mimeType: finalMime },
|
|
587
|
+
{ maxBytes: MAX_IMAGE_BYTES },
|
|
588
|
+
);
|
|
589
|
+
base64Data = compressed.data;
|
|
590
|
+
finalMime = compressed.mimeType;
|
|
591
|
+
} catch {
|
|
592
|
+
this.ctx.showStatus(
|
|
593
|
+
`Image too large: ${filePath} (${(sizeBytes / 1024 / 1024).toFixed(1)} MB, max 5 MB)`,
|
|
594
|
+
);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
this.ctx.pendingImages.push({ type: "image", data: base64Data, mimeType: finalMime });
|
|
600
|
+
const imageNum = this.ctx.pendingImages.length;
|
|
601
|
+
this.ctx.editor.insertText(`[Image #${imageNum}] `);
|
|
602
|
+
loaded++;
|
|
603
|
+
} catch {
|
|
604
|
+
this.ctx.showStatus(`Failed to read image: ${filePath}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (loaded > 0) {
|
|
608
|
+
this.ctx.ui.requestRender();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
#highlightEditorLine(text: string): string {
|
|
613
|
+
// Highlight [Image #N] and [paste #N ...] markers with accent color
|
|
614
|
+
return text.replace(/\[(Image #\d+|paste #\d+[^\]]*)\]/g, match => {
|
|
615
|
+
return theme.fg("accent", match);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
502
619
|
/** Copy current prompt text to system clipboard. */
|
|
503
620
|
handleCopyPrompt(): void {
|
|
504
621
|
const text = this.ctx.editor.getText();
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Text,
|
|
16
16
|
TUI,
|
|
17
17
|
} from "@nghyane/arcane-tui";
|
|
18
|
-
import {
|
|
18
|
+
import { isEnoent, logger, postmortem } from "@nghyane/arcane-utils";
|
|
19
19
|
import { APP_NAME, getProjectDir } from "@nghyane/arcane-utils/dirs";
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import { KeybindingsManager } from "../config/keybindings";
|
|
@@ -27,7 +27,6 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
|
27
27
|
import { HistoryStorage } from "../session/history-storage";
|
|
28
28
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
29
29
|
import { getRecentSessions } from "../session/session-manager";
|
|
30
|
-
import { STTController, type SttState } from "../stt";
|
|
31
30
|
import { setMermaidRenderCallback } from "../theme/mermaid-cache";
|
|
32
31
|
import type { Theme } from "../theme/theme";
|
|
33
32
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "../theme/theme";
|
|
@@ -145,11 +144,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
145
144
|
readonly #inputController: InputController;
|
|
146
145
|
readonly #selectorController: SelectorController;
|
|
147
146
|
readonly #uiHelpers: UiHelpers;
|
|
148
|
-
#sttController: STTController | undefined;
|
|
149
|
-
#voiceAnimationInterval: NodeJS.Timeout | undefined;
|
|
150
|
-
#voiceHue = 0;
|
|
151
|
-
#voicePreviousShowHardwareCursor: boolean | null = null;
|
|
152
|
-
#voicePreviousUseTerminalCursor: boolean | null = null;
|
|
153
147
|
#resizeHandler?: () => void;
|
|
154
148
|
|
|
155
149
|
constructor(
|
|
@@ -501,11 +495,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
501
495
|
this.loadingAnimation.stop();
|
|
502
496
|
this.loadingAnimation = undefined;
|
|
503
497
|
}
|
|
504
|
-
this.#cleanupMicAnimation();
|
|
505
|
-
if (this.#sttController) {
|
|
506
|
-
this.#sttController.dispose();
|
|
507
|
-
this.#sttController = undefined;
|
|
508
|
-
}
|
|
509
498
|
this.#extensionUiController.clearExtensionTerminalInputListeners();
|
|
510
499
|
this.statusLine.dispose();
|
|
511
500
|
if (this.#resizeHandler) {
|
|
@@ -726,78 +715,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
726
715
|
return this.#commandController.handleMemoryCommand(text);
|
|
727
716
|
}
|
|
728
717
|
|
|
729
|
-
async handleSTTToggle(): Promise<void> {
|
|
730
|
-
if (!settings.get("stt.enabled")) {
|
|
731
|
-
this.showWarning("Speech-to-text is disabled. Enable it in settings: stt.enabled");
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (!this.#sttController) {
|
|
735
|
-
this.#sttController = new STTController();
|
|
736
|
-
}
|
|
737
|
-
await this.#sttController.toggle(this.editor, {
|
|
738
|
-
showWarning: (msg: string) => this.showWarning(msg),
|
|
739
|
-
showStatus: (msg: string) => this.showStatus(msg),
|
|
740
|
-
onStateChange: (state: SttState) => {
|
|
741
|
-
if (state === "recording") {
|
|
742
|
-
this.#voicePreviousShowHardwareCursor = this.ui.getShowHardwareCursor();
|
|
743
|
-
this.#voicePreviousUseTerminalCursor = this.editor.getUseTerminalCursor();
|
|
744
|
-
this.ui.setShowHardwareCursor(false);
|
|
745
|
-
this.editor.setUseTerminalCursor(false);
|
|
746
|
-
this.#startMicAnimation();
|
|
747
|
-
} else if (state === "transcribing") {
|
|
748
|
-
this.#stopMicAnimation();
|
|
749
|
-
this.editor.cursorOverride = `\x1b[38;2;200;200;200m${theme.icon.mic}\x1b[0m`;
|
|
750
|
-
this.editor.cursorOverrideWidth = 1;
|
|
751
|
-
} else {
|
|
752
|
-
this.#cleanupMicAnimation();
|
|
753
|
-
}
|
|
754
|
-
this.updateEditorTopBorder();
|
|
755
|
-
this.ui.requestRender();
|
|
756
|
-
},
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
#updateMicIcon(): void {
|
|
761
|
-
const { r, g, b } = hsvToRgb({ h: this.#voiceHue, s: 0.9, v: 1.0 });
|
|
762
|
-
this.editor.cursorOverride = `\x1b[38;2;${r};${g};${b}m${theme.icon.mic}\x1b[0m`;
|
|
763
|
-
this.editor.cursorOverrideWidth = 1;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
#startMicAnimation(): void {
|
|
767
|
-
if (this.#voiceAnimationInterval) return;
|
|
768
|
-
this.#voiceHue = 0;
|
|
769
|
-
this.#updateMicIcon();
|
|
770
|
-
this.#voiceAnimationInterval = setInterval(() => {
|
|
771
|
-
this.#voiceHue = (this.#voiceHue + 8) % 360;
|
|
772
|
-
this.#updateMicIcon();
|
|
773
|
-
this.ui.requestRender();
|
|
774
|
-
}, 60);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
#stopMicAnimation(): void {
|
|
778
|
-
if (this.#voiceAnimationInterval) {
|
|
779
|
-
clearInterval(this.#voiceAnimationInterval);
|
|
780
|
-
this.#voiceAnimationInterval = undefined;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
#cleanupMicAnimation(): void {
|
|
785
|
-
if (this.#voiceAnimationInterval) {
|
|
786
|
-
clearInterval(this.#voiceAnimationInterval);
|
|
787
|
-
this.#voiceAnimationInterval = undefined;
|
|
788
|
-
}
|
|
789
|
-
this.editor.cursorOverride = undefined;
|
|
790
|
-
this.editor.cursorOverrideWidth = undefined;
|
|
791
|
-
if (this.#voicePreviousShowHardwareCursor !== null) {
|
|
792
|
-
this.ui.setShowHardwareCursor(this.#voicePreviousShowHardwareCursor);
|
|
793
|
-
this.#voicePreviousShowHardwareCursor = null;
|
|
794
|
-
}
|
|
795
|
-
if (this.#voicePreviousUseTerminalCursor !== null) {
|
|
796
|
-
this.editor.setUseTerminalCursor(this.#voicePreviousUseTerminalCursor);
|
|
797
|
-
this.#voicePreviousUseTerminalCursor = null;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
718
|
showDebugSelector(): void {
|
|
802
719
|
this.#selectorController.showDebugSelector();
|
|
803
720
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -149,7 +149,6 @@ export interface InteractiveModeContext {
|
|
|
149
149
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
150
150
|
handleMoveCommand(targetPath: string): Promise<void>;
|
|
151
151
|
handleMemoryCommand(text: string): Promise<void>;
|
|
152
|
-
handleSTTToggle(): Promise<void>;
|
|
153
152
|
executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
|
|
154
153
|
openInBrowser(urlOrPath: string): void;
|
|
155
154
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|