@miphamai/cli 0.44.4 → 0.45.1
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 +1 -1
- package/src/core/engine.ts +9 -0
- package/src/daemon/remote-engine.ts +3 -0
- package/src/providers/anthropic.ts +17 -8
- package/src/providers/fetch-utils.ts +22 -0
- package/src/providers/openai-compat.ts +17 -9
- package/src/providers/registry.ts +2 -0
- package/src/security/path.ts +32 -0
- package/src/shared/package-info.ts +1 -1
- package/src/skills/mipham/runtime.ts +6 -3
- package/src/skills/standard/runtime.ts +6 -3
- package/src/tools/exec/bash.ts +7 -0
- package/src/tools/file/grep.ts +60 -19
- package/src/ui/app.tsx +1 -0
- package/src/ui/chat.tsx +4 -0
package/package.json
CHANGED
package/src/core/engine.ts
CHANGED
|
@@ -86,6 +86,8 @@ export class QueryEngine {
|
|
|
86
86
|
private goal?: string
|
|
87
87
|
private maxGoalLoops = 20
|
|
88
88
|
private lastAssistantContent?: string
|
|
89
|
+
/** Reasoning effort (low|medium|high|xhigh|max) — scales the provider idle timeout. */
|
|
90
|
+
private effort: string = 'high'
|
|
89
91
|
/** Custom verification shell script path for goal checking. */
|
|
90
92
|
private goalVerifyScript?: string
|
|
91
93
|
/** Custom verification skill name for goal checking. */
|
|
@@ -117,6 +119,11 @@ export class QueryEngine {
|
|
|
117
119
|
dialogExpiry: 300,
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
/** Set the reasoning effort (low|medium|high|xhigh|max). */
|
|
123
|
+
setEffort(level: string): void {
|
|
124
|
+
this.effort = level
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
/** Set the session identifier (called from index.tsx at startup with the actual session name). */
|
|
121
128
|
setSessionId(id: string): void {
|
|
122
129
|
this.sessionId = id
|
|
@@ -1209,6 +1216,7 @@ export class QueryEngine {
|
|
|
1209
1216
|
systemPrompt,
|
|
1210
1217
|
tools: toolDefs,
|
|
1211
1218
|
signal,
|
|
1219
|
+
effort: this.effort,
|
|
1212
1220
|
})) {
|
|
1213
1221
|
if (chunk.type === 'error') {
|
|
1214
1222
|
failure = chunk.error ?? 'Unknown error'
|
|
@@ -1253,6 +1261,7 @@ export class QueryEngine {
|
|
|
1253
1261
|
systemPrompt,
|
|
1254
1262
|
tools: toolDefs,
|
|
1255
1263
|
signal,
|
|
1264
|
+
effort: this.effort,
|
|
1256
1265
|
})) {
|
|
1257
1266
|
yield chunk
|
|
1258
1267
|
}
|
|
@@ -224,6 +224,9 @@ export class RemoteEngine {
|
|
|
224
224
|
/** No-op: provider switching is managed by the daemon session. */
|
|
225
225
|
switchProvider(_providerId: string, _modelId?: string): void {}
|
|
226
226
|
|
|
227
|
+
/** No-op: reasoning effort is managed by the daemon session. */
|
|
228
|
+
setEffort(_level: string): void {}
|
|
229
|
+
|
|
227
230
|
/** Remote mode has no local agent registry. */
|
|
228
231
|
getAgentRegistry(): undefined {
|
|
229
232
|
return undefined
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
ContentBlock,
|
|
7
7
|
} from '../shared/index.ts'
|
|
8
8
|
import type { ProviderInstance, ChatRequest } from './registry'
|
|
9
|
-
import { fetchWithRetry } from './fetch-utils'
|
|
9
|
+
import { fetchWithRetry, streamIdleTimeoutMs } from './fetch-utils'
|
|
10
10
|
|
|
11
11
|
interface AnthropicContentBlock {
|
|
12
12
|
type: string
|
|
@@ -109,24 +109,33 @@ export class AnthropicProvider implements ProviderInstance {
|
|
|
109
109
|
const decoder = new TextDecoder()
|
|
110
110
|
let buffer = ''
|
|
111
111
|
|
|
112
|
-
// Streaming
|
|
113
|
-
|
|
112
|
+
// Streaming idle timeout: scaled by reasoning effort so extended thinking
|
|
113
|
+
// passes aren't mistaken for a stalled connection.
|
|
114
|
+
const STREAM_READ_TIMEOUT_MS = streamIdleTimeoutMs(req.effort)
|
|
114
115
|
|
|
115
116
|
while (true) {
|
|
116
117
|
let readResult: Awaited<ReturnType<typeof reader.read>>
|
|
118
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined
|
|
117
119
|
try {
|
|
118
120
|
readResult = await Promise.race([
|
|
119
121
|
reader.read(),
|
|
120
|
-
new Promise<never>((_, reject) =>
|
|
121
|
-
setTimeout(
|
|
122
|
-
() =>
|
|
122
|
+
new Promise<never>((_, reject) => {
|
|
123
|
+
idleTimer = setTimeout(
|
|
124
|
+
() =>
|
|
125
|
+
reject(
|
|
126
|
+
new Error(
|
|
127
|
+
`Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
|
|
128
|
+
),
|
|
129
|
+
),
|
|
123
130
|
STREAM_READ_TIMEOUT_MS,
|
|
124
|
-
)
|
|
125
|
-
),
|
|
131
|
+
)
|
|
132
|
+
}),
|
|
126
133
|
])
|
|
127
134
|
} catch (err) {
|
|
128
135
|
yield { type: 'error', error: `Stream stalled: ${String(err)}` }
|
|
129
136
|
return
|
|
137
|
+
} finally {
|
|
138
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
130
139
|
}
|
|
131
140
|
const { done, value } = readResult
|
|
132
141
|
if (done) break
|
|
@@ -80,6 +80,28 @@ function sleep(ms: number): Promise<void> {
|
|
|
80
80
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// ── Streaming idle-timeout (stall guard) with effort multiplier ──────────
|
|
84
|
+
//
|
|
85
|
+
// Reasoning models think silently for long stretches. The idle timeout between
|
|
86
|
+
// stream chunks is scaled by reasoning effort so a long thinking pass isn't
|
|
87
|
+
// mistaken for a stalled connection (jcode stall-guard alignment).
|
|
88
|
+
|
|
89
|
+
/** Base idle timeout between stream chunks, in ms. */
|
|
90
|
+
export const STREAM_IDLE_TIMEOUT_BASE_MS = 90_000
|
|
91
|
+
|
|
92
|
+
/** Effort level → idle-timeout multiplier. Unknown/absent effort → 1× (base). */
|
|
93
|
+
const EFFORT_TIMEOUT_MULTIPLIER: Record<string, number> = {
|
|
94
|
+
high: 2,
|
|
95
|
+
xhigh: 3,
|
|
96
|
+
max: 4,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compute the streaming idle timeout for a given reasoning-effort level. */
|
|
100
|
+
export function streamIdleTimeoutMs(effort?: string): number {
|
|
101
|
+
const multiplier = effort ? (EFFORT_TIMEOUT_MULTIPLIER[effort] ?? 1) : 1
|
|
102
|
+
return STREAM_IDLE_TIMEOUT_BASE_MS * multiplier
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
/**
|
|
84
106
|
* Combine multiple AbortSignals into one — any signal aborting
|
|
85
107
|
* triggers the combined signal.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
2
|
import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
|
|
3
3
|
import type { ProviderInstance, ChatRequest } from './registry'
|
|
4
|
-
import { fetchWithRetry } from './fetch-utils'
|
|
4
|
+
import { fetchWithRetry, streamIdleTimeoutMs } from './fetch-utils'
|
|
5
5
|
import { OLLAMA_PRESET_MODELS } from '../shared/constants'
|
|
6
6
|
|
|
7
7
|
export class OpenAICompatProvider implements ProviderInstance {
|
|
@@ -54,25 +54,33 @@ export class OpenAICompatProvider implements ProviderInstance {
|
|
|
54
54
|
const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
|
|
55
55
|
let reasoningContent = ''
|
|
56
56
|
|
|
57
|
-
// Streaming
|
|
58
|
-
// DeepSeek V4
|
|
59
|
-
const STREAM_READ_TIMEOUT_MS =
|
|
57
|
+
// Streaming idle timeout: scaled by reasoning effort so long thinking passes
|
|
58
|
+
// (DeepSeek V4 / reasoning models) aren't mistaken for a stalled connection.
|
|
59
|
+
const STREAM_READ_TIMEOUT_MS = streamIdleTimeoutMs(req.effort)
|
|
60
60
|
|
|
61
61
|
while (true) {
|
|
62
62
|
let readResult: Awaited<ReturnType<typeof reader.read>>
|
|
63
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined
|
|
63
64
|
try {
|
|
64
65
|
readResult = await Promise.race([
|
|
65
66
|
reader.read(),
|
|
66
|
-
new Promise<never>((_, reject) =>
|
|
67
|
-
setTimeout(
|
|
68
|
-
() =>
|
|
67
|
+
new Promise<never>((_, reject) => {
|
|
68
|
+
idleTimer = setTimeout(
|
|
69
|
+
() =>
|
|
70
|
+
reject(
|
|
71
|
+
new Error(
|
|
72
|
+
`Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
69
75
|
STREAM_READ_TIMEOUT_MS,
|
|
70
|
-
)
|
|
71
|
-
),
|
|
76
|
+
)
|
|
77
|
+
}),
|
|
72
78
|
])
|
|
73
79
|
} catch (err) {
|
|
74
80
|
yield { type: 'error', error: `Stream stalled: ${String(err)}` }
|
|
75
81
|
return
|
|
82
|
+
} finally {
|
|
83
|
+
if (idleTimer) clearTimeout(idleTimer)
|
|
76
84
|
}
|
|
77
85
|
const { done, value } = readResult
|
|
78
86
|
if (done) break
|
|
@@ -17,6 +17,8 @@ export interface ChatRequest {
|
|
|
17
17
|
maxTokens?: number
|
|
18
18
|
temperature?: number
|
|
19
19
|
signal?: AbortSignal
|
|
20
|
+
/** Reasoning effort (low|medium|high|xhigh|max) — scales the streaming idle timeout. */
|
|
21
|
+
effort?: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export class ProviderRegistry implements Llm {
|
package/src/security/path.ts
CHANGED
|
@@ -7,6 +7,30 @@ import { realpathSync, existsSync } from 'node:fs'
|
|
|
7
7
|
*/
|
|
8
8
|
const BLOCKED_PATHS = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root']
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Windows UNC paths and NT/Win32/DOS device-namespace prefixes.
|
|
12
|
+
*
|
|
13
|
+
* Accessing a UNC path (\\server\share) triggers SMB negotiation on Windows,
|
|
14
|
+
* which silently sends NTLM credentials to the target — a credential-leak
|
|
15
|
+
* vector (relay / offline cracking). The NT-namespace spelling (\??\UNC\...)
|
|
16
|
+
* bypasses naive UNC checks, so all device-namespace prefixes are rejected too.
|
|
17
|
+
*
|
|
18
|
+
* Anchored at token boundaries so escaped backslashes (echo \\n) and local
|
|
19
|
+
* drive paths (C:\\Users\\me) are NOT flagged, and http:// URLs are excluded
|
|
20
|
+
* from the forward-slash form.
|
|
21
|
+
*/
|
|
22
|
+
const UNC_DEVICE_PATTERNS: RegExp[] = [
|
|
23
|
+
/(?:^|[\s"'`(;|&])\\\\[A-Za-z0-9._-]+\\[^\s"'`;|&]/,
|
|
24
|
+
/(?:^|[\s"'`(;|&])\/\/[A-Za-z0-9._-]+\/[^\s"'`;|&]/,
|
|
25
|
+
/\\\?\?\\/,
|
|
26
|
+
/\\\\\?\\/,
|
|
27
|
+
/\\\\\.\\/,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function isUncOrDevicePath(path: string): boolean {
|
|
31
|
+
return UNC_DEVICE_PATTERNS.some((re) => re.test(path))
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
/**
|
|
11
35
|
* Resolve a user-supplied path relative to cwd, with sandbox enforcement.
|
|
12
36
|
*
|
|
@@ -22,6 +46,14 @@ const BLOCKED_PATHS = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root']
|
|
|
22
46
|
* @throws {Error} with a human-readable message if the path is blocked.
|
|
23
47
|
*/
|
|
24
48
|
export function resolveSafe(cwd: string, inputPath: string): string {
|
|
49
|
+
// Reject UNC / device-namespace paths before touching the filesystem —
|
|
50
|
+
// resolving a UNC path on Windows would trigger SMB negotiation (NTLM leak).
|
|
51
|
+
if (isUncOrDevicePath(inputPath)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Path rejected: "${inputPath}" is a UNC or device-namespace path (blocked to prevent NTLM credential leakage).`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
const raw = resolve(cwd, inputPath)
|
|
26
58
|
const normalized = normalize(raw)
|
|
27
59
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.45.1' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
|
@@ -54,9 +54,12 @@ export class MiphamRuntime {
|
|
|
54
54
|
...variables,
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// Single-pass substitution: replacement values are NOT re-scanned for more
|
|
58
|
+
// template markers, so a variable containing `${key}` can't be re-expanded
|
|
59
|
+
// into another variable's value.
|
|
60
|
+
result = result.replace(/\$\{([^}]+)\}/g, (match, key: string) =>
|
|
61
|
+
Object.prototype.hasOwnProperty.call(allVars, key) ? allVars[key]! : match,
|
|
62
|
+
)
|
|
60
63
|
|
|
61
64
|
return result
|
|
62
65
|
}
|
|
@@ -49,9 +49,12 @@ export class StandardRuntime {
|
|
|
49
49
|
|
|
50
50
|
let result = prompt
|
|
51
51
|
if (variables) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// Single-pass substitution: replacement values are NOT re-scanned for
|
|
53
|
+
// more template markers, so an argument containing `${key}` can't be
|
|
54
|
+
// re-expanded into another variable's value.
|
|
55
|
+
result = result.replace(/\$\{([^}]+)\}/g, (match, key: string) =>
|
|
56
|
+
Object.prototype.hasOwnProperty.call(variables, key) ? variables[key]! : match,
|
|
57
|
+
)
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
return result
|
package/src/tools/exec/bash.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolDefinition, CredentialMaskingConfig } from '../../shared/index.ts'
|
|
2
2
|
import { sanitizeCommand } from '../../shared/sanitize.ts'
|
|
3
3
|
import { DANGEROUS_GIT_PATTERNS } from './git.ts'
|
|
4
|
+
import { isUncOrDevicePath } from '../../security/path.ts'
|
|
4
5
|
import type { Service } from '../../vajra'
|
|
5
6
|
import { toolKey } from '../seam'
|
|
6
7
|
import { withValidation } from '../validation'
|
|
@@ -137,6 +138,12 @@ export function isBlocked(command: string): string | null {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// Reject UNC / device-namespace paths — accessing one triggers SMB negotiation
|
|
142
|
+
// on Windows and silently leaks NTLM credentials (Claude Code 2.1.233 CVE-class fix).
|
|
143
|
+
if (isUncOrDevicePath(command) || isUncOrDevicePath(normalized) || isUncOrDevicePath(sanitized)) {
|
|
144
|
+
return `Command rejected: references a UNC or device-namespace path (blocked to prevent NTLM credential leakage).`
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
// Check dangerous patterns (on original, normalized, and sanitized)
|
|
141
148
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
142
149
|
if (pattern.test(command) || pattern.test(normalized) || pattern.test(sanitized)) {
|
package/src/tools/file/grep.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import { $ } from 'bun'
|
|
2
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
2
|
import { resolveSafe } from '../../security/path'
|
|
4
3
|
|
|
4
|
+
/** Grep stall guard: a search over a huge tree (or a pathological regex) must
|
|
5
|
+
* not hang the session for minutes. Matches the Bash tool's 120s ceiling. */
|
|
6
|
+
const GREP_TIMEOUT_MS = 120_000
|
|
7
|
+
|
|
8
|
+
/** Run a search command with a kill-timer; report whether the timeout fired. */
|
|
9
|
+
export async function runSearch(
|
|
10
|
+
cmd: string[],
|
|
11
|
+
cwd: string,
|
|
12
|
+
timeoutMs: number,
|
|
13
|
+
): Promise<{ stdout: string; timedOut: boolean; exitCode: number | null }> {
|
|
14
|
+
const proc = Bun.spawn(cmd, { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
15
|
+
let timedOut = false
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
timedOut = true
|
|
18
|
+
proc.kill()
|
|
19
|
+
}, timeoutMs)
|
|
20
|
+
const stdout = await new Response(proc.stdout).text()
|
|
21
|
+
await proc.exited
|
|
22
|
+
clearTimeout(timer)
|
|
23
|
+
return { stdout, timedOut, exitCode: proc.exitCode }
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export const grepTool: ToolDefinition = {
|
|
6
27
|
name: 'Grep',
|
|
7
28
|
description:
|
|
@@ -29,32 +50,52 @@ export const grepTool: ToolDefinition = {
|
|
|
29
50
|
const searchPath = resolveSafe(ctx.cwd, (params.path as string) || '.')
|
|
30
51
|
const include = params.include as string | undefined
|
|
31
52
|
|
|
53
|
+
// 1. ripgrep (fast path)
|
|
54
|
+
const rgArgs = ['rg', '-n', '--heading', '--color=never', '-M', '500', pattern]
|
|
55
|
+
if (include) rgArgs.push('--glob', include)
|
|
56
|
+
rgArgs.push(searchPath)
|
|
32
57
|
try {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
args.push(searchPath)
|
|
36
|
-
|
|
37
|
-
const result = await $`rg ${args}`.cwd(ctx.cwd).quiet().text()
|
|
38
|
-
return { success: true, content: result || '(no matches)' }
|
|
39
|
-
} catch (err: unknown) {
|
|
40
|
-
const exitErr = err as { exitCode?: number }
|
|
41
|
-
if (exitErr.exitCode === 1) {
|
|
42
|
-
return { success: true, content: '(no matches)' }
|
|
43
|
-
}
|
|
44
|
-
// Fallback to grep
|
|
45
|
-
try {
|
|
46
|
-
const content = await $`grep -rn ${pattern} ${searchPath}`.cwd(ctx.cwd).quiet().text()
|
|
58
|
+
const { stdout, timedOut, exitCode } = await runSearch(rgArgs, ctx.cwd, GREP_TIMEOUT_MS)
|
|
59
|
+
if (timedOut) {
|
|
47
60
|
return {
|
|
48
|
-
success:
|
|
49
|
-
content:
|
|
61
|
+
success: false,
|
|
62
|
+
content: '',
|
|
63
|
+
error: `Grep timed out after ${GREP_TIMEOUT_MS / 1000}s — narrow scope with "path" and "include".`,
|
|
50
64
|
}
|
|
51
|
-
}
|
|
65
|
+
}
|
|
66
|
+
if (exitCode === 1) return { success: true, content: '(no matches)' }
|
|
67
|
+
if (exitCode === 0) return { success: true, content: stdout || '(no matches)' }
|
|
68
|
+
// rg exit 2 (error) or null (killed) → fall through to grep
|
|
69
|
+
} catch {
|
|
70
|
+
// rg not installed → fall through to grep
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. fallback to plain grep
|
|
74
|
+
const grepArgs = ['grep', '-rn', pattern, searchPath]
|
|
75
|
+
try {
|
|
76
|
+
const { stdout, timedOut, exitCode } = await runSearch(grepArgs, ctx.cwd, GREP_TIMEOUT_MS)
|
|
77
|
+
if (timedOut) {
|
|
52
78
|
return {
|
|
53
79
|
success: false,
|
|
54
80
|
content: '',
|
|
55
|
-
error:
|
|
81
|
+
error: `Grep timed out after ${GREP_TIMEOUT_MS / 1000}s — narrow scope with "path" and "include".`,
|
|
56
82
|
}
|
|
57
83
|
}
|
|
84
|
+
if (exitCode === 1) return { success: true, content: '(no matches)' }
|
|
85
|
+
if (exitCode === 0) {
|
|
86
|
+
return { success: true, content: stdout.slice(0, 50000) || '(no matches)' }
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
content: '',
|
|
91
|
+
error: 'grep failed. Install ripgrep: brew install ripgrep',
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
content: '',
|
|
97
|
+
error: 'grep failed. Install ripgrep: brew install ripgrep',
|
|
98
|
+
}
|
|
58
99
|
}
|
|
59
100
|
},
|
|
60
101
|
}
|
package/src/ui/app.tsx
CHANGED
|
@@ -278,6 +278,7 @@ export function App({
|
|
|
278
278
|
setEffort: (level: string) => {
|
|
279
279
|
setEffort(level)
|
|
280
280
|
setPreference('lastCodeReviewEffort', level)
|
|
281
|
+
engine.setEffort(level)
|
|
281
282
|
},
|
|
282
283
|
setFocusMode: (on: boolean) => setFocusMode(on),
|
|
283
284
|
setGoal: (text: string) => setGoalText(text),
|
package/src/ui/chat.tsx
CHANGED
|
@@ -83,6 +83,10 @@ const MessageRow = React.memo(
|
|
|
83
83
|
<Text bold color="green">
|
|
84
84
|
▸ {displayCwd()}:
|
|
85
85
|
</Text>
|
|
86
|
+
) : msg.role === 'assistant' ? (
|
|
87
|
+
<Text bold color="magenta">
|
|
88
|
+
◆ {t('ui.assistant.role_label')}:
|
|
89
|
+
</Text>
|
|
86
90
|
) : msg.role === 'system' ? (
|
|
87
91
|
<Text bold color="yellow">
|
|
88
92
|
⚠ {t('ui.system.role_label')}:
|