@nghyane/arcane 0.1.19 → 0.1.20
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 +22 -0
- package/package.json +7 -7
- package/src/lsp/clients/biome-client.ts +1 -1
- package/src/lsp/edits.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +3 -2
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +2 -2
- package/src/modes/components/assistant-message.ts +55 -25
- package/src/modes/components/bash-execution.ts +31 -0
- package/src/modes/components/context-group.ts +30 -3
- package/src/modes/components/model-selector.ts +35 -9
- package/src/modes/components/python-execution.ts +37 -0
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/event-controller.ts +43 -11
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/edit-tool.ts +13 -24
- package/src/patch/hashline.ts +105 -3
- package/src/patch/schemas.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/session/agent-session.ts +28 -27
- package/src/task/index.ts +1 -9
- package/src/task/render.ts +3 -3
- package/src/tools/ask.ts +0 -2
- package/src/tools/bash.ts +6 -3
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +7 -5
- package/src/tools/fetch.ts +5 -2
- package/src/tools/find-thread.ts +5 -2
- package/src/tools/find.ts +3 -3
- package/src/tools/gemini-image.ts +18 -10
- package/src/tools/github.ts +2 -2
- package/src/tools/grep.ts +3 -3
- package/src/tools/notebook.ts +8 -2
- package/src/tools/python.ts +3 -2
- package/src/tools/read-thread.ts +5 -2
- package/src/tools/read.ts +6 -3
- package/src/tools/render-mermaid.ts +3 -7
- package/src/tools/save-memory.ts +6 -3
- package/src/tools/ssh.ts +6 -3
- package/src/tools/todo-write.ts +6 -3
- package/src/tools/undo-edit.ts +5 -2
- package/src/ui/render-utils.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/web/github-client.ts +2 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/render.ts +11 -2
- package/src/prompts/tools/render-mermaid.md +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.20] - 2026-03-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Spinner animation on tool calls while running
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Stricter tool schemas and simplified edit tool insert operation
|
|
14
|
+
- Improved edit error steering and diff preview
|
|
15
|
+
- Normalize node:path/fs imports to namespace style
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Task tool returns plain text instead of JSON-serialized result
|
|
20
|
+
- GitHub client non-JSON response handling
|
|
21
|
+
- Tab sanitization in hashline
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- Reuse markdown components and throttle stream renders
|
|
26
|
+
|
|
5
27
|
## [0.1.17] - 2026-03-02
|
|
6
28
|
|
|
7
29
|
### Added
|
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.20",
|
|
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,12 +44,12 @@
|
|
|
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.
|
|
52
|
-
"@nghyane/arcane-utils": "^0.1.
|
|
47
|
+
"@nghyane/arcane-stats": "^0.1.12",
|
|
48
|
+
"@nghyane/arcane-agent": "^0.1.16",
|
|
49
|
+
"@nghyane/arcane-ai": "^0.1.12",
|
|
50
|
+
"@nghyane/arcane-natives": "^0.1.11",
|
|
51
|
+
"@nghyane/arcane-tui": "^0.1.15",
|
|
52
|
+
"@nghyane/arcane-utils": "^0.1.8",
|
|
53
53
|
"@sinclair/typebox": "^0.34.48",
|
|
54
54
|
"@xterm/headless": "^6.0.0",
|
|
55
55
|
"ajv": "^8.18.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Biome CLI-based linter client.
|
|
3
3
|
* Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
|
|
4
4
|
*/
|
|
5
|
-
import path from "node:path";
|
|
5
|
+
import * as path from "node:path";
|
|
6
6
|
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
package/src/lsp/edits.ts
CHANGED
package/src/lsp/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
2
|
+
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
|
|
4
4
|
import { logger, once, untilAborted } from "@nghyane/arcane-utils";
|
|
5
5
|
import type { BunFile } from "bun";
|
package/src/lsp/render.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { LspParams, LspToolDetails } from "./types";
|
|
|
16
16
|
* Render the LSP tool call in the TUI.
|
|
17
17
|
* Shows: "lsp <operation> <file/filecount>"
|
|
18
18
|
*/
|
|
19
|
-
export function renderCall(args: LspParams,
|
|
19
|
+
export function renderCall(args: LspParams, options: RenderResultOptions, theme: Theme): Text {
|
|
20
20
|
const actionLabel = (args.action ?? "request").replace(/_/g, " ");
|
|
21
21
|
const queryPreview = args.query ? truncateToWidth(args.query, TRUNCATE_LENGTHS.SHORT) : undefined;
|
|
22
22
|
|
|
@@ -66,7 +66,8 @@ export function renderCall(args: LspParams, _options: RenderResultOptions, theme
|
|
|
66
66
|
|
|
67
67
|
const text = renderStatusLine(
|
|
68
68
|
{
|
|
69
|
-
icon: "
|
|
69
|
+
icon: "running",
|
|
70
|
+
spinnerFrame: options.spinnerFrame,
|
|
70
71
|
title: "LSP",
|
|
71
72
|
description: descriptionParts.join(" "),
|
|
72
73
|
meta,
|
package/src/lsp/utils.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import * as nodeFs from "node:fs";
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
@@ -284,7 +284,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
const normalizePath = (value: string) => {
|
|
287
|
-
const resolved = realpathSync(path.resolve(value));
|
|
287
|
+
const resolved = nodeFs.realpathSync(path.resolve(value));
|
|
288
288
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
289
289
|
};
|
|
290
290
|
|
|
@@ -4,13 +4,21 @@ import { logger } from "@nghyane/arcane-utils";
|
|
|
4
4
|
import { hasPendingMermaid, prerenderMermaid } from "../../theme/mermaid-cache";
|
|
5
5
|
import { getMarkdownTheme, theme } from "../../theme/theme";
|
|
6
6
|
|
|
7
|
+
interface CachedBlock {
|
|
8
|
+
type: "text" | "thinking" | "thinking-hidden";
|
|
9
|
+
component: Markdown | Text;
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
|
-
* Component that renders a complete assistant message
|
|
14
|
+
* Component that renders a complete assistant message.
|
|
15
|
+
* Reuses Markdown/Text instances across updates so unchanged blocks skip re-parsing.
|
|
9
16
|
*/
|
|
10
17
|
export class AssistantMessageComponent extends Container {
|
|
11
18
|
#contentContainer: Container;
|
|
12
19
|
#lastMessage?: AssistantMessage;
|
|
13
20
|
#prerenderInFlight = false;
|
|
21
|
+
#cachedBlocks: CachedBlock[] = [];
|
|
14
22
|
|
|
15
23
|
constructor(
|
|
16
24
|
message?: AssistantMessage,
|
|
@@ -69,11 +77,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
69
77
|
|
|
70
78
|
updateContent(message: AssistantMessage): void {
|
|
71
79
|
this.#lastMessage = message;
|
|
72
|
-
|
|
73
|
-
// Clear content container
|
|
74
80
|
this.#contentContainer.clear();
|
|
75
|
-
|
|
76
|
-
// Trigger background mermaid pre-rendering if needed
|
|
77
81
|
this.#triggerMermaidPrerender(message);
|
|
78
82
|
|
|
79
83
|
const hasVisibleContent = message.content.some(
|
|
@@ -84,43 +88,73 @@ export class AssistantMessageComponent extends Container {
|
|
|
84
88
|
this.#contentContainer.addChild(new Spacer(1));
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
let blockIndex = 0;
|
|
88
92
|
for (let i = 0; i < message.content.length; i++) {
|
|
89
93
|
const content = message.content[i];
|
|
90
94
|
if (content.type === "text" && content.text.trim()) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const text = content.text.trim();
|
|
96
|
+
const cached = this.#cachedBlocks[blockIndex];
|
|
97
|
+
let md: Markdown;
|
|
98
|
+
if (cached?.type === "text") {
|
|
99
|
+
md = cached.component as Markdown;
|
|
100
|
+
if (cached.text !== text) {
|
|
101
|
+
md.setText(text);
|
|
102
|
+
cached.text = text;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
md = new Markdown(text, 2, 0, getMarkdownTheme());
|
|
106
|
+
this.#cachedBlocks[blockIndex] = { type: "text", component: md, text };
|
|
107
|
+
}
|
|
108
|
+
this.#contentContainer.addChild(md);
|
|
109
|
+
blockIndex++;
|
|
94
110
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
95
|
-
// Add spacing only when another visible assistant content block follows.
|
|
96
|
-
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
97
111
|
const hasVisibleContentAfter = message.content
|
|
98
112
|
.slice(i + 1)
|
|
99
113
|
.some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
|
|
100
114
|
|
|
101
115
|
if (this.hideThinkingBlock) {
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
const cached = this.#cachedBlocks[blockIndex];
|
|
117
|
+
let label: Text;
|
|
118
|
+
if (cached?.type === "thinking-hidden") {
|
|
119
|
+
label = cached.component as Text;
|
|
120
|
+
} else {
|
|
121
|
+
label = new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 2, 0);
|
|
122
|
+
this.#cachedBlocks[blockIndex] = { type: "thinking-hidden", component: label, text: "" };
|
|
123
|
+
}
|
|
124
|
+
this.#contentContainer.addChild(label);
|
|
104
125
|
if (hasVisibleContentAfter) {
|
|
105
126
|
this.#contentContainer.addChild(new Spacer(1));
|
|
106
127
|
}
|
|
107
128
|
} else {
|
|
108
|
-
|
|
109
|
-
this.#
|
|
110
|
-
|
|
129
|
+
const text = content.thinking.trim();
|
|
130
|
+
const cached = this.#cachedBlocks[blockIndex];
|
|
131
|
+
let md: Markdown;
|
|
132
|
+
if (cached?.type === "thinking") {
|
|
133
|
+
md = cached.component as Markdown;
|
|
134
|
+
if (cached.text !== text) {
|
|
135
|
+
md.setText(text);
|
|
136
|
+
cached.text = text;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
md = new Markdown(text, 2, 0, getMarkdownTheme(), {
|
|
111
140
|
color: (text: string) => theme.fg("thinkingText", text),
|
|
112
141
|
italic: true,
|
|
113
|
-
})
|
|
114
|
-
|
|
142
|
+
});
|
|
143
|
+
this.#cachedBlocks[blockIndex] = { type: "thinking", component: md, text };
|
|
144
|
+
}
|
|
145
|
+
this.#contentContainer.addChild(md);
|
|
115
146
|
if (hasVisibleContentAfter) {
|
|
116
147
|
this.#contentContainer.addChild(new Spacer(1));
|
|
117
148
|
}
|
|
118
149
|
}
|
|
150
|
+
blockIndex++;
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
if (this.#cachedBlocks.length > blockIndex) {
|
|
155
|
+
this.#cachedBlocks.length = blockIndex;
|
|
156
|
+
}
|
|
157
|
+
|
|
124
158
|
const hasToolCalls = message.content.some(c => c.type === "toolCall");
|
|
125
159
|
if (!hasToolCalls) {
|
|
126
160
|
if (message.stopReason === "aborted") {
|
|
@@ -128,11 +162,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
128
162
|
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
129
163
|
? message.errorMessage
|
|
130
164
|
: "Operation aborted";
|
|
131
|
-
|
|
132
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
133
|
-
} else {
|
|
134
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
135
|
-
}
|
|
165
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
136
166
|
this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 2, 0));
|
|
137
167
|
} else if (message.stopReason === "error") {
|
|
138
168
|
const errorMsg = message.errorMessage || "Unknown error";
|
|
@@ -22,6 +22,9 @@ export class BashExecutionComponent extends Container {
|
|
|
22
22
|
#expanded = false;
|
|
23
23
|
#headerText: Text;
|
|
24
24
|
#bodyText: Text;
|
|
25
|
+
#spinnerFrame = 0;
|
|
26
|
+
#spinnerInterval?: NodeJS.Timeout;
|
|
27
|
+
#ui: TUI;
|
|
25
28
|
|
|
26
29
|
constructor(
|
|
27
30
|
private readonly command: string,
|
|
@@ -29,6 +32,7 @@ export class BashExecutionComponent extends Container {
|
|
|
29
32
|
_excludeFromContext = false,
|
|
30
33
|
) {
|
|
31
34
|
super();
|
|
35
|
+
this.#ui = ui;
|
|
32
36
|
this.addChild(new Spacer(1));
|
|
33
37
|
|
|
34
38
|
this.#headerText = new Text(
|
|
@@ -49,6 +53,7 @@ export class BashExecutionComponent extends Container {
|
|
|
49
53
|
getSymbolTheme().spinnerFrames,
|
|
50
54
|
);
|
|
51
55
|
this.addChild(this.#loader);
|
|
56
|
+
this.#startSpinner();
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
setExpanded(expanded: boolean): void {
|
|
@@ -91,9 +96,28 @@ export class BashExecutionComponent extends Container {
|
|
|
91
96
|
this.#setOutput(options.output);
|
|
92
97
|
}
|
|
93
98
|
this.#loader.stop();
|
|
99
|
+
this.#stopSpinner();
|
|
94
100
|
this.#updateDisplay();
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
#startSpinner(): void {
|
|
104
|
+
if (this.#spinnerInterval) return;
|
|
105
|
+
this.#spinnerInterval = setInterval(() => {
|
|
106
|
+
const frameCount = theme.spinnerFrames.length;
|
|
107
|
+
if (frameCount === 0) return;
|
|
108
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
109
|
+
this.#updateDisplay();
|
|
110
|
+
this.#ui.requestRender();
|
|
111
|
+
}, 80);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#stopSpinner(): void {
|
|
115
|
+
if (this.#spinnerInterval) {
|
|
116
|
+
clearInterval(this.#spinnerInterval);
|
|
117
|
+
this.#spinnerInterval = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
#updateDisplay(): void {
|
|
98
122
|
const isError = this.#status === "error";
|
|
99
123
|
const isDone = this.#status !== "running";
|
|
@@ -109,6 +133,13 @@ export class BashExecutionComponent extends Container {
|
|
|
109
133
|
this.#headerText.setText(
|
|
110
134
|
renderStatusLine({ icon, title: "Bash", description: `$ ${this.command}`, meta }, theme),
|
|
111
135
|
);
|
|
136
|
+
} else {
|
|
137
|
+
this.#headerText.setText(
|
|
138
|
+
renderStatusLine(
|
|
139
|
+
{ icon: "running", spinnerFrame: this.#spinnerFrame, title: "Bash", description: `$ ${this.command}` },
|
|
140
|
+
theme,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
112
143
|
}
|
|
113
144
|
|
|
114
145
|
// Build tree-style body
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Component } from "@nghyane/arcane-tui";
|
|
1
|
+
import type { Component, TUI } from "@nghyane/arcane-tui";
|
|
2
2
|
import { Spacer } from "@nghyane/arcane-tui";
|
|
3
3
|
import { theme } from "../../theme/theme";
|
|
4
4
|
import { formatCount, formatStatusIcon } from "../../ui/render-utils";
|
|
@@ -26,19 +26,27 @@ export class ContextGroupComponent implements Component {
|
|
|
26
26
|
#expanded = false;
|
|
27
27
|
#pendingCount = 0;
|
|
28
28
|
#spacer: Spacer;
|
|
29
|
+
#spinnerFrame = 0;
|
|
30
|
+
#spinnerInterval?: NodeJS.Timeout;
|
|
31
|
+
#ui: TUI;
|
|
29
32
|
|
|
30
|
-
constructor() {
|
|
33
|
+
constructor(ui: TUI) {
|
|
31
34
|
this.#spacer = new Spacer(1);
|
|
35
|
+
this.#ui = ui;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
addTool(name: string, component: ToolExecutionComponent): void {
|
|
35
39
|
this.#entries.push({ name, component });
|
|
36
40
|
this.#pendingCount++;
|
|
37
41
|
component.setExpanded(this.#expanded);
|
|
42
|
+
this.#startSpinner();
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
markDone(): void {
|
|
41
46
|
this.#pendingCount = Math.max(0, this.#pendingCount - 1);
|
|
47
|
+
if (this.#pendingCount <= 0) {
|
|
48
|
+
this.#stopSpinner();
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
setExpanded(expanded: boolean): void {
|
|
@@ -63,6 +71,23 @@ export class ContextGroupComponent implements Component {
|
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
#startSpinner(): void {
|
|
75
|
+
if (this.#spinnerInterval) return;
|
|
76
|
+
this.#spinnerInterval = setInterval(() => {
|
|
77
|
+
const frameCount = theme.spinnerFrames.length;
|
|
78
|
+
if (frameCount === 0) return;
|
|
79
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
80
|
+
this.#ui.requestRender();
|
|
81
|
+
}, 80);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#stopSpinner(): void {
|
|
85
|
+
if (this.#spinnerInterval) {
|
|
86
|
+
clearInterval(this.#spinnerInterval);
|
|
87
|
+
this.#spinnerInterval = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
render(width: number): string[] {
|
|
67
92
|
const lines: string[] = [];
|
|
68
93
|
|
|
@@ -71,7 +96,9 @@ export class ContextGroupComponent implements Component {
|
|
|
71
96
|
|
|
72
97
|
// Summary line
|
|
73
98
|
const allDone = this.#pendingCount <= 0;
|
|
74
|
-
const icon = allDone
|
|
99
|
+
const icon = allDone
|
|
100
|
+
? formatStatusIcon("success", theme)
|
|
101
|
+
: formatStatusIcon("running", theme, this.#spinnerFrame);
|
|
75
102
|
const label = allDone ? "Gathered context" : "Gathering context…";
|
|
76
103
|
|
|
77
104
|
// Count by tool type
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
visibleWidth,
|
|
13
13
|
} from "@nghyane/arcane-tui";
|
|
14
14
|
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
|
|
15
|
-
import { parseModelString } from "../../config/model-resolver";
|
|
15
|
+
import { parseModelPattern, parseModelString } from "../../config/model-resolver";
|
|
16
16
|
import type { Settings } from "../../config/settings";
|
|
17
17
|
import { type ThemeColor, theme } from "../../theme/theme";
|
|
18
18
|
import { fuzzyFilter } from "../../utils/fuzzy";
|
|
@@ -24,6 +24,19 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
|
|
|
24
24
|
return `${bgAnsi}\x1b[30m ${label} \x1b[39m${theme.getAppBgAnsi()}`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function formatRoleThinkingLabel(thinkingLevel: string | undefined): string {
|
|
28
|
+
if (!thinkingLevel || thinkingLevel === "default") return "inherit";
|
|
29
|
+
const labels: Record<string, string> = {
|
|
30
|
+
minimal: "min",
|
|
31
|
+
low: "low",
|
|
32
|
+
medium: "med",
|
|
33
|
+
high: "high",
|
|
34
|
+
xhigh: "xhi",
|
|
35
|
+
off: "off",
|
|
36
|
+
};
|
|
37
|
+
return labels[thinkingLevel] ?? thinkingLevel;
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
interface ModelItem {
|
|
28
41
|
provider: string;
|
|
29
42
|
id: string;
|
|
@@ -40,7 +53,11 @@ interface MenuAction {
|
|
|
40
53
|
role: ModelRole;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
|
-
const MENU_ACTIONS: MenuAction[] = MODEL_ROLE_IDS.map(role =>
|
|
56
|
+
const MENU_ACTIONS: MenuAction[] = MODEL_ROLE_IDS.map(role => {
|
|
57
|
+
const roleInfo = MODEL_ROLES[role];
|
|
58
|
+
const roleLabel = roleInfo.tag ? `${roleInfo.tag} (${roleInfo.name})` : roleInfo.name;
|
|
59
|
+
return { label: `Set as ${roleLabel}`, role };
|
|
60
|
+
});
|
|
44
61
|
|
|
45
62
|
const ALL_TAB = "ALL";
|
|
46
63
|
|
|
@@ -69,7 +86,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
69
86
|
#allModels: ModelItem[] = [];
|
|
70
87
|
#filteredModels: ModelItem[] = [];
|
|
71
88
|
#selectedIndex: number = 0;
|
|
72
|
-
#roles: { [key in ModelRole]?: Model } = {};
|
|
89
|
+
#roles: { [key in ModelRole]?: { model: Model; thinkingLevel?: string } } = {};
|
|
73
90
|
#settings: Settings;
|
|
74
91
|
#modelRegistry: ModelRegistry;
|
|
75
92
|
#onSelectCallback: (model: Model, role: ModelRole | null) => void;
|
|
@@ -183,9 +200,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
183
200
|
if (parsed) {
|
|
184
201
|
const model = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
185
202
|
if (model) {
|
|
186
|
-
this.#roles[role] = model;
|
|
203
|
+
this.#roles[role] = { model };
|
|
204
|
+
continue;
|
|
187
205
|
}
|
|
188
206
|
}
|
|
207
|
+
// Fallback: parse as pattern to extract thinking level from suffix
|
|
208
|
+
const result = parseModelPattern(modelId, allModels);
|
|
209
|
+
if (result.model) {
|
|
210
|
+
this.#roles[role] = { model: result.model, thinkingLevel: result.thinkingLevel };
|
|
211
|
+
}
|
|
189
212
|
}
|
|
190
213
|
}
|
|
191
214
|
|
|
@@ -198,7 +221,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
198
221
|
let i = 0;
|
|
199
222
|
while (i < MODEL_ROLE_IDS.length) {
|
|
200
223
|
const role = MODEL_ROLE_IDS[i];
|
|
201
|
-
if (this.#roles[role] && modelsAreEqual(this.#roles[role], model.model)) {
|
|
224
|
+
if (this.#roles[role] && modelsAreEqual(this.#roles[role]!.model, model.model)) {
|
|
202
225
|
break;
|
|
203
226
|
}
|
|
204
227
|
i++;
|
|
@@ -395,9 +418,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
395
418
|
const badges: string[] = [];
|
|
396
419
|
for (const role of MODEL_ROLE_IDS) {
|
|
397
420
|
const { tag, color } = MODEL_ROLES[role];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
421
|
+
const assigned = this.#roles[role];
|
|
422
|
+
if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
|
|
423
|
+
|
|
424
|
+
const badge = makeInvertedBadge(tag, color ?? "success");
|
|
425
|
+
const thinkingLabel = formatRoleThinkingLabel(assigned.thinkingLevel);
|
|
426
|
+
badges.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
|
|
401
427
|
}
|
|
402
428
|
const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
|
|
403
429
|
|
|
@@ -596,7 +622,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
596
622
|
this.#settings.setModelRole(role, `${model.provider}/${model.id}`);
|
|
597
623
|
|
|
598
624
|
// Update local state for UI
|
|
599
|
-
this.#roles[role] = model;
|
|
625
|
+
this.#roles[role] = { model };
|
|
600
626
|
|
|
601
627
|
// Notify caller (for updating agent state if needed)
|
|
602
628
|
this.#onSelectCallback(model, role);
|
|
@@ -22,6 +22,9 @@ export class PythonExecutionComponent extends Container {
|
|
|
22
22
|
#expanded = false;
|
|
23
23
|
#headerText: Text;
|
|
24
24
|
#bodyText: Text;
|
|
25
|
+
#spinnerFrame = 0;
|
|
26
|
+
#spinnerInterval?: NodeJS.Timeout;
|
|
27
|
+
#ui: TUI;
|
|
25
28
|
|
|
26
29
|
constructor(
|
|
27
30
|
private readonly code: string,
|
|
@@ -29,6 +32,7 @@ export class PythonExecutionComponent extends Container {
|
|
|
29
32
|
_excludeFromContext = false,
|
|
30
33
|
) {
|
|
31
34
|
super();
|
|
35
|
+
this.#ui = ui;
|
|
32
36
|
this.addChild(new Spacer(1));
|
|
33
37
|
|
|
34
38
|
const codePreview = code.split("\n")[0].slice(0, 60);
|
|
@@ -50,6 +54,7 @@ export class PythonExecutionComponent extends Container {
|
|
|
50
54
|
getSymbolTheme().spinnerFrames,
|
|
51
55
|
);
|
|
52
56
|
this.addChild(this.#loader);
|
|
57
|
+
this.#startSpinner();
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
setExpanded(expanded: boolean): void {
|
|
@@ -92,9 +97,28 @@ export class PythonExecutionComponent extends Container {
|
|
|
92
97
|
this.#setOutput(options.output);
|
|
93
98
|
}
|
|
94
99
|
this.#loader.stop();
|
|
100
|
+
this.#stopSpinner();
|
|
95
101
|
this.#updateDisplay();
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
#startSpinner(): void {
|
|
105
|
+
if (this.#spinnerInterval) return;
|
|
106
|
+
this.#spinnerInterval = setInterval(() => {
|
|
107
|
+
const frameCount = theme.spinnerFrames.length;
|
|
108
|
+
if (frameCount === 0) return;
|
|
109
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
110
|
+
this.#updateDisplay();
|
|
111
|
+
this.#ui.requestRender();
|
|
112
|
+
}, 80);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#stopSpinner(): void {
|
|
116
|
+
if (this.#spinnerInterval) {
|
|
117
|
+
clearInterval(this.#spinnerInterval);
|
|
118
|
+
this.#spinnerInterval = undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
#updateDisplay(): void {
|
|
99
123
|
const isError = this.#status === "error";
|
|
100
124
|
const isDone = this.#status !== "running";
|
|
@@ -110,6 +134,19 @@ export class PythonExecutionComponent extends Container {
|
|
|
110
134
|
this.#headerText.setText(
|
|
111
135
|
renderStatusLine({ icon, title: "Python", description: `>>> ${codePreview}`, meta }, theme),
|
|
112
136
|
);
|
|
137
|
+
} else {
|
|
138
|
+
const codePreview = this.code.split("\n")[0].slice(0, 60);
|
|
139
|
+
this.#headerText.setText(
|
|
140
|
+
renderStatusLine(
|
|
141
|
+
{
|
|
142
|
+
icon: "running",
|
|
143
|
+
spinnerFrame: this.#spinnerFrame,
|
|
144
|
+
title: "Python",
|
|
145
|
+
description: `>>> ${codePreview}`,
|
|
146
|
+
},
|
|
147
|
+
theme,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
113
150
|
}
|
|
114
151
|
|
|
115
152
|
const bodyLines: string[] = [];
|
|
@@ -130,6 +130,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
130
130
|
this.addChild(this.#contentBox);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
this.#updateSpinnerAnimation();
|
|
133
134
|
this.#updateDisplay();
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -212,10 +213,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
212
213
|
* Start or stop spinner animation based on whether this is a partial task result.
|
|
213
214
|
*/
|
|
214
215
|
#updateSpinnerAnimation(): void {
|
|
215
|
-
// Spinner
|
|
216
|
-
const
|
|
217
|
-
const isPartialTask = this.#isPartial && this.#tool?.mergeCallAndResult === true;
|
|
218
|
-
const needsSpinner = isStreamingArgs || isPartialTask;
|
|
216
|
+
// Spinner runs whenever the tool hasn't produced a final result
|
|
217
|
+
const needsSpinner = this.#isPartial || !this.#result || !this.#argsComplete;
|
|
219
218
|
if (needsSpinner && !this.#spinnerInterval) {
|
|
220
219
|
this.#spinnerInterval = setInterval(() => {
|
|
221
220
|
const frameCount = theme.spinnerFrames.length;
|