pi-rtk-optimizer 0.6.0 → 0.7.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/CHANGELOG.md +24 -0
- package/README.md +19 -12
- package/config/config.example.json +6 -3
- package/package.json +67 -64
- package/src/additional-coverage-test.ts +151 -50
- package/src/command-rewriter-test.ts +187 -118
- package/src/command-rewriter.ts +5 -2
- package/src/config-modal-test.ts +95 -29
- package/src/config-modal.ts +29 -3
- package/src/config-store.ts +32 -5
- package/src/index-test.ts +227 -3
- package/src/index.ts +50 -5
- package/src/output-compactor-test.ts +45 -2
- package/src/output-compactor.ts +26 -15
- package/src/rewrite-pipeline-safety.ts +203 -157
- package/src/rtk-command-environment.ts +13 -4
- package/src/rtk-executable-resolver.ts +97 -0
- package/src/rtk-rewrite-provider.ts +39 -3
- package/src/shell-env-prefix.ts +5 -1
- package/src/test-helpers.ts +23 -10
- package/src/tool-execution-sanitizer.ts +80 -69
- package/src/types.ts +13 -3
- package/src/windows-command-helpers.ts +92 -16
- package/src/zellij-modal.ts +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { resolveRtkExecutable, type RtkExecutableResolution } from "./rtk-executable-resolver.js";
|
|
2
3
|
|
|
3
4
|
export interface RtkRewriteProviderResult {
|
|
4
5
|
changed: boolean;
|
|
@@ -6,6 +7,14 @@ export interface RtkRewriteProviderResult {
|
|
|
6
7
|
rewrittenCommand: string;
|
|
7
8
|
exitCode: number;
|
|
8
9
|
error?: string;
|
|
10
|
+
executableResolution?: RtkExecutableResolution;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RtkRewriteProviderOptions {
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
resolverTimeoutMs?: number;
|
|
16
|
+
platform?: typeof process.platform;
|
|
17
|
+
executableResolution?: RtkExecutableResolution;
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
function isAlreadyRtk(command: string): boolean {
|
|
@@ -13,11 +22,21 @@ function isAlreadyRtk(command: string): boolean {
|
|
|
13
22
|
return trimmed === "rtk" || trimmed.startsWith("rtk ");
|
|
14
23
|
}
|
|
15
24
|
|
|
25
|
+
function normalizeOptions(optionsOrTimeout: number | RtkRewriteProviderOptions): RtkRewriteProviderOptions {
|
|
26
|
+
if (typeof optionsOrTimeout === "number") {
|
|
27
|
+
return { timeoutMs: optionsOrTimeout };
|
|
28
|
+
}
|
|
29
|
+
return optionsOrTimeout;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export async function resolveRtkRewrite(
|
|
17
33
|
pi: ExtensionAPI,
|
|
18
34
|
command: string,
|
|
19
|
-
|
|
35
|
+
optionsOrTimeout: number | RtkRewriteProviderOptions = {},
|
|
20
36
|
): Promise<RtkRewriteProviderResult> {
|
|
37
|
+
const options = normalizeOptions(optionsOrTimeout);
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
39
|
+
|
|
21
40
|
if (!command || !command.trim()) {
|
|
22
41
|
return { changed: false, originalCommand: command, rewrittenCommand: command, exitCode: 1 };
|
|
23
42
|
}
|
|
@@ -27,10 +46,22 @@ export async function resolveRtkRewrite(
|
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
try {
|
|
30
|
-
const
|
|
49
|
+
const executableResolution =
|
|
50
|
+
options.executableResolution ??
|
|
51
|
+
(await resolveRtkExecutable(pi, {
|
|
52
|
+
platform: options.platform,
|
|
53
|
+
timeoutMs: options.resolverTimeoutMs,
|
|
54
|
+
}));
|
|
55
|
+
const result = await pi.exec(executableResolution.command, ["rewrite", command], { timeout: timeoutMs });
|
|
31
56
|
|
|
32
57
|
if (result.code === 1) {
|
|
33
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
changed: false,
|
|
60
|
+
originalCommand: command,
|
|
61
|
+
rewrittenCommand: command,
|
|
62
|
+
exitCode: 1,
|
|
63
|
+
executableResolution,
|
|
64
|
+
};
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
if (result.code === 2) {
|
|
@@ -40,6 +71,7 @@ export async function resolveRtkRewrite(
|
|
|
40
71
|
rewrittenCommand: command,
|
|
41
72
|
exitCode: 2,
|
|
42
73
|
error: result.stderr?.trim() || "rtk denied rewrite",
|
|
74
|
+
executableResolution,
|
|
43
75
|
};
|
|
44
76
|
}
|
|
45
77
|
|
|
@@ -52,6 +84,7 @@ export async function resolveRtkRewrite(
|
|
|
52
84
|
rewrittenCommand: command,
|
|
53
85
|
exitCode: result.code,
|
|
54
86
|
error: "rtk returned empty output",
|
|
87
|
+
executableResolution,
|
|
55
88
|
};
|
|
56
89
|
}
|
|
57
90
|
if (rewritten === command) {
|
|
@@ -60,6 +93,7 @@ export async function resolveRtkRewrite(
|
|
|
60
93
|
originalCommand: command,
|
|
61
94
|
rewrittenCommand: command,
|
|
62
95
|
exitCode: result.code,
|
|
96
|
+
executableResolution,
|
|
63
97
|
};
|
|
64
98
|
}
|
|
65
99
|
return {
|
|
@@ -67,6 +101,7 @@ export async function resolveRtkRewrite(
|
|
|
67
101
|
originalCommand: command,
|
|
68
102
|
rewrittenCommand: rewritten,
|
|
69
103
|
exitCode: result.code,
|
|
104
|
+
executableResolution,
|
|
70
105
|
};
|
|
71
106
|
}
|
|
72
107
|
|
|
@@ -76,6 +111,7 @@ export async function resolveRtkRewrite(
|
|
|
76
111
|
rewrittenCommand: command,
|
|
77
112
|
exitCode: result.code,
|
|
78
113
|
error: `unexpected exit code ${result.code}`,
|
|
114
|
+
executableResolution,
|
|
79
115
|
};
|
|
80
116
|
} catch (error) {
|
|
81
117
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/shell-env-prefix.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const SINGLE_QUOTED_SHELL_VALUE_PATTERN = "'(?:'\\\\''|[^'])*'";
|
|
2
|
+
const ENV_ASSIGNMENT_VALUE_PATTERN = `(?:"[^"]*"|${SINGLE_QUOTED_SHELL_VALUE_PATTERN}|[^\\s]+)`;
|
|
3
|
+
const LEADING_ENV_ASSIGNMENT_PATTERN = new RegExp(
|
|
4
|
+
`^((?:[A-Za-z_][A-Za-z0-9_]*=${ENV_ASSIGNMENT_VALUE_PATTERN}\\s+)*)`,
|
|
5
|
+
);
|
|
2
6
|
|
|
3
7
|
export interface LeadingEnvAssignmentSplit {
|
|
4
8
|
envPrefix: string;
|
package/src/test-helpers.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { DEFAULT_RTK_INTEGRATION_CONFIG, type RtkIntegrationConfig } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
type TestResult = void | Promise<void>;
|
|
4
|
+
|
|
5
|
+
function isPromiseLike(value: TestResult): value is Promise<void> {
|
|
6
|
+
return Boolean(value && typeof (value as Promise<void>).then === "function");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function runTest(name: string, testFn: () => TestResult): TestResult {
|
|
10
|
+
const result = testFn();
|
|
11
|
+
if (!isPromiseLike(result)) {
|
|
12
|
+
console.log(`[PASS] ${name}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result.then(() => {
|
|
17
|
+
console.log(`[PASS] ${name}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function cloneDefaultConfig(): RtkIntegrationConfig {
|
|
22
|
+
return structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG);
|
|
23
|
+
}
|
|
@@ -1,69 +1,80 @@
|
|
|
1
|
-
import { toRecord } from "./record-utils.js";
|
|
2
|
-
import { sanitizeRtkEmojiOutput, stripAnsiFast, stripRtkHookWarnings } from "./techniques/index.js";
|
|
3
|
-
|
|
4
|
-
interface ToolResultTextBlock {
|
|
5
|
-
type: string;
|
|
6
|
-
text?: string;
|
|
7
|
-
[key: string]: unknown;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
nextText =
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
import { toRecord } from "./record-utils.js";
|
|
2
|
+
import { sanitizeRtkEmojiOutput, stripAnsiFast, stripRtkHookWarnings } from "./techniques/index.js";
|
|
3
|
+
|
|
4
|
+
interface ToolResultTextBlock {
|
|
5
|
+
type: string;
|
|
6
|
+
text?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface StreamingBashExecutionSanitizationResult {
|
|
11
|
+
changed: boolean;
|
|
12
|
+
result: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sanitizeStreamingBashText(text: string, command: string | undefined | null): string {
|
|
16
|
+
let nextText = stripAnsiFast(text);
|
|
17
|
+
|
|
18
|
+
const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
|
|
19
|
+
if (withoutRtkHookWarnings !== null) {
|
|
20
|
+
nextText = withoutRtkHookWarnings;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
|
|
24
|
+
if (withoutRtkEmoji !== null) {
|
|
25
|
+
nextText = withoutRtkEmoji;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return nextText;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a sanitized shallow copy of streamed bash result blocks before the
|
|
33
|
+
* TUI renders them so RTK self-diagnostics never flash in partial or final
|
|
34
|
+
* tool output. The input object is not mutated.
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeStreamingBashExecutionResult(
|
|
37
|
+
result: unknown,
|
|
38
|
+
command: string | undefined | null,
|
|
39
|
+
): StreamingBashExecutionSanitizationResult {
|
|
40
|
+
const resultRecord = toRecord(result);
|
|
41
|
+
const sourceContent = Array.isArray(resultRecord.content) ? resultRecord.content : null;
|
|
42
|
+
if (!sourceContent || sourceContent.length === 0) {
|
|
43
|
+
return { changed: false, result };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let changed = false;
|
|
47
|
+
const nextContent = sourceContent.map((block) => {
|
|
48
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) {
|
|
49
|
+
return block;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const contentBlock = block as ToolResultTextBlock;
|
|
53
|
+
if (contentBlock.type !== "text" || typeof contentBlock.text !== "string") {
|
|
54
|
+
return block;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sanitizedText = sanitizeStreamingBashText(contentBlock.text, command);
|
|
58
|
+
if (sanitizedText === contentBlock.text) {
|
|
59
|
+
return block;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
changed = true;
|
|
63
|
+
return {
|
|
64
|
+
...contentBlock,
|
|
65
|
+
text: sanitizedText,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!changed) {
|
|
70
|
+
return { changed: false, result };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
changed: true,
|
|
75
|
+
result: {
|
|
76
|
+
...resultRecord,
|
|
77
|
+
content: nextContent,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,9 @@ export type RtkSourceFilterLevel = (typeof RTK_SOURCE_FILTER_LEVELS)[number];
|
|
|
7
7
|
export interface RtkOutputCompactionConfig {
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
stripAnsi: boolean;
|
|
10
|
+
readCompaction: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
};
|
|
10
13
|
truncate: {
|
|
11
14
|
enabled: boolean;
|
|
12
15
|
maxChars: number;
|
|
@@ -42,15 +45,18 @@ export const DEFAULT_RTK_INTEGRATION_CONFIG: RtkIntegrationConfig = {
|
|
|
42
45
|
outputCompaction: {
|
|
43
46
|
enabled: true,
|
|
44
47
|
stripAnsi: true,
|
|
48
|
+
readCompaction: {
|
|
49
|
+
enabled: false,
|
|
50
|
+
},
|
|
45
51
|
truncate: {
|
|
46
52
|
enabled: true,
|
|
47
53
|
maxChars: 12_000,
|
|
48
54
|
},
|
|
49
|
-
sourceCodeFilteringEnabled:
|
|
55
|
+
sourceCodeFilteringEnabled: false,
|
|
50
56
|
preserveExactSkillReads: false,
|
|
51
|
-
sourceCodeFiltering: "
|
|
57
|
+
sourceCodeFiltering: "none",
|
|
52
58
|
smartTruncate: {
|
|
53
|
-
enabled:
|
|
59
|
+
enabled: false,
|
|
54
60
|
maxLines: 220,
|
|
55
61
|
},
|
|
56
62
|
aggregateTestOutput: true,
|
|
@@ -81,5 +87,9 @@ export interface RuntimeStatus {
|
|
|
81
87
|
rtkAvailable: boolean;
|
|
82
88
|
lastCheckedAt?: number;
|
|
83
89
|
lastError?: string;
|
|
90
|
+
rtkExecutablePath?: string;
|
|
91
|
+
rtkExecutableCommand?: string;
|
|
92
|
+
rtkExecutableResolver?: string;
|
|
93
|
+
rtkExecutableResolutionWarning?: string;
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -3,6 +3,12 @@ interface WindowsBashCompatibilityResult {
|
|
|
3
3
|
applied: string[];
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
interface LeadingCdSlashDParse {
|
|
7
|
+
rawPath: string;
|
|
8
|
+
operator: string;
|
|
9
|
+
tail: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
const PYTHON_UTF8_ENV_PREFIX = "PYTHONIOENCODING=utf-8";
|
|
7
13
|
|
|
8
14
|
function normalizeWindowsPathForBash(rawPath: string): string {
|
|
@@ -20,29 +26,96 @@ function quoteForBash(value: string): string {
|
|
|
20
26
|
return `"${escaped}"`;
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
function parseLeadingCdSlashD(command: string): LeadingCdSlashDParse | null {
|
|
30
|
+
const prefixMatch = command.match(/^\s*cd\s+\/d\s+/i);
|
|
31
|
+
if (!prefixMatch) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pathStart = prefixMatch[0].length;
|
|
36
|
+
let quote: '"' | "'" | null = null;
|
|
37
|
+
let escaped = false;
|
|
38
|
+
|
|
39
|
+
for (let index = pathStart; index < command.length; index += 1) {
|
|
40
|
+
const character = command[index] ?? "";
|
|
41
|
+
const nextCharacter = command[index + 1] ?? "";
|
|
42
|
+
|
|
43
|
+
if (escaped) {
|
|
44
|
+
escaped = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (quote !== null) {
|
|
49
|
+
if (character === "\\" && quote !== "'") {
|
|
50
|
+
escaped = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (character === quote) {
|
|
54
|
+
quote = null;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (character === "\\") {
|
|
60
|
+
escaped = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (character === '"' || character === "'") {
|
|
65
|
+
quote = character;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (character === "&" && nextCharacter === "&") {
|
|
70
|
+
return {
|
|
71
|
+
rawPath: command.slice(pathStart, index),
|
|
72
|
+
operator: "&&",
|
|
73
|
+
tail: command.slice(index + 2),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (character === "|" && nextCharacter === "|") {
|
|
78
|
+
return {
|
|
79
|
+
rawPath: command.slice(pathStart, index),
|
|
80
|
+
operator: "||",
|
|
81
|
+
tail: command.slice(index + 2),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (character === "|" || character === ";") {
|
|
86
|
+
return {
|
|
87
|
+
rawPath: command.slice(pathStart, index),
|
|
88
|
+
operator: character,
|
|
89
|
+
tail: command.slice(index + 1),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
rawPath: command.slice(pathStart),
|
|
96
|
+
operator: "",
|
|
97
|
+
tail: "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
function rewriteLeadingCdSlashD(command: string): { command: string; changed: boolean } {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
const tail = withTailMatch[2] ?? "";
|
|
28
|
-
const normalizedPath = quoteForBash(normalizeWindowsPathForBash(rawPath));
|
|
29
|
-
return {
|
|
30
|
-
command: `cd ${normalizedPath} && ${tail}`,
|
|
31
|
-
changed: true,
|
|
32
|
-
};
|
|
102
|
+
const parsed = parseLeadingCdSlashD(command);
|
|
103
|
+
if (!parsed) {
|
|
104
|
+
return { command, changed: false };
|
|
33
105
|
}
|
|
34
106
|
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
const rawPath = onlyCdMatch[1] ?? "";
|
|
38
|
-
const normalizedPath = quoteForBash(normalizeWindowsPathForBash(rawPath));
|
|
107
|
+
const normalizedPath = quoteForBash(normalizeWindowsPathForBash(parsed.rawPath));
|
|
108
|
+
if (!parsed.operator) {
|
|
39
109
|
return {
|
|
40
110
|
command: `cd ${normalizedPath}`,
|
|
41
111
|
changed: true,
|
|
42
112
|
};
|
|
43
113
|
}
|
|
44
114
|
|
|
45
|
-
return {
|
|
115
|
+
return {
|
|
116
|
+
command: `cd ${normalizedPath} ${parsed.operator} ${parsed.tail.trimStart()}`,
|
|
117
|
+
changed: true,
|
|
118
|
+
};
|
|
46
119
|
}
|
|
47
120
|
|
|
48
121
|
function ensurePythonUtf8(command: string): { command: string; changed: boolean } {
|
|
@@ -60,8 +133,11 @@ function ensurePythonUtf8(command: string): { command: string; changed: boolean
|
|
|
60
133
|
};
|
|
61
134
|
}
|
|
62
135
|
|
|
63
|
-
export function applyWindowsBashCompatibilityFixes(
|
|
64
|
-
|
|
136
|
+
export function applyWindowsBashCompatibilityFixes(
|
|
137
|
+
command: string,
|
|
138
|
+
platform: string = process.platform,
|
|
139
|
+
): WindowsBashCompatibilityResult {
|
|
140
|
+
if (platform !== "win32") {
|
|
65
141
|
return { command, applied: [] };
|
|
66
142
|
}
|
|
67
143
|
|
package/src/zellij-modal.ts
CHANGED
|
@@ -831,7 +831,7 @@ export class ZellijSettingsModal implements ZellijModalContentRenderer {
|
|
|
831
831
|
options.settings,
|
|
832
832
|
Math.min(Math.max(options.settings.length + 2, 6), 18),
|
|
833
833
|
getSettingsListTheme(),
|
|
834
|
-
(id, value) => {
|
|
834
|
+
(id: string, value: string) => {
|
|
835
835
|
this.options.onChange(id, value);
|
|
836
836
|
},
|
|
837
837
|
() => {
|