tinker-agent 1.2.1 → 1.4.0
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 +26 -1
- package/README.md +217 -72
- package/bin/tinker.js +75 -25
- package/package.json +8 -3
- package/src/agent/loop.ts +115 -11
- package/src/agent/runtime-session.ts +34 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +131 -264
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +143 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +650 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +100 -0
- package/src/cli/tui-runner.tsx +52 -49
- package/src/events/observation-text-log.ts +2 -0
- package/src/events/stdout-event-printer.ts +7 -1
- package/src/events/types.ts +27 -3
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/model/model-client.ts +29 -0
- package/src/model/model-context-profile.ts +0 -30
- package/src/model/openai-chat-mapping.ts +54 -15
- package/src/model/openai-chat-model-client.ts +46 -27
- package/src/model/openai-chat-stream.ts +6 -2
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +15 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +24 -7
- package/src/tui/slash-commands.ts +76 -24
- package/src/tui/workspace-file-search.ts +78 -71
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { ImageAssetId, ImageAssetRef } from "../image/image-types";
|
|
12
12
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
13
13
|
import type { InputTokenEstimator } from "./input-token-estimator";
|
|
14
|
-
import { ModelRequestMediaAggregateError } from "./model-client";
|
|
14
|
+
import { ModelRequestMediaAggregateError, ProviderResponseError } from "./model-client";
|
|
15
15
|
import type {
|
|
16
16
|
MaterializedModelRequest,
|
|
17
17
|
ModelClient,
|
|
@@ -234,17 +234,12 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
234
234
|
throw new Error("Image request must be materialized before provider dispatch.");
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{ signal: options.signal },
|
|
244
|
-
);
|
|
245
|
-
} catch (error) {
|
|
246
|
-
throw sanitizedProviderError(error);
|
|
247
|
-
}
|
|
237
|
+
const response = this.stream
|
|
238
|
+
? accumulateOpenAIChatCompletionChunks(
|
|
239
|
+
await this.collectStreamingChunks(prepared, options.signal),
|
|
240
|
+
{ provider: this.provider, model: this.options.model },
|
|
241
|
+
)
|
|
242
|
+
: await this.requestNonStreaming(prepared, options.signal);
|
|
248
243
|
|
|
249
244
|
return fromOpenAIChatCompletion(response, {
|
|
250
245
|
identity: options.identity,
|
|
@@ -253,22 +248,37 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
253
248
|
});
|
|
254
249
|
}
|
|
255
250
|
|
|
256
|
-
private async
|
|
251
|
+
private async collectStreamingChunks(
|
|
257
252
|
prepared: PreparedModelRequest,
|
|
258
253
|
signal: AbortSignal,
|
|
259
|
-
): Promise<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
254
|
+
): Promise<unknown[]> {
|
|
255
|
+
try {
|
|
256
|
+
const stream = await this.client.chat.completions.create(
|
|
257
|
+
prepared.payload as ChatCompletionCreateParamsStreaming,
|
|
258
|
+
{ signal },
|
|
259
|
+
);
|
|
260
|
+
const chunks: unknown[] = [];
|
|
261
|
+
for await (const chunk of stream) {
|
|
262
|
+
chunks.push(chunk);
|
|
263
|
+
}
|
|
264
|
+
return chunks;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
throw sanitizedProviderError(error, this.provider, this.options.model);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async requestNonStreaming(
|
|
271
|
+
prepared: PreparedModelRequest,
|
|
272
|
+
signal: AbortSignal,
|
|
273
|
+
) {
|
|
274
|
+
try {
|
|
275
|
+
return await this.client.chat.completions.create(
|
|
276
|
+
prepared.payload as ChatCompletionCreateParamsNonStreaming,
|
|
277
|
+
{ signal },
|
|
278
|
+
);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw sanitizedProviderError(error, this.provider, this.options.model);
|
|
267
281
|
}
|
|
268
|
-
return accumulateOpenAIChatCompletionChunks(chunks, {
|
|
269
|
-
provider: this.provider,
|
|
270
|
-
model: this.options.model,
|
|
271
|
-
});
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
private assistantReplaySegments(message: AssistantMessage): PreparedPromptSegment[] {
|
|
@@ -508,7 +518,11 @@ function deepFreeze<T>(value: T): T {
|
|
|
508
518
|
return Object.freeze(value);
|
|
509
519
|
}
|
|
510
520
|
|
|
511
|
-
function sanitizedProviderError(
|
|
521
|
+
function sanitizedProviderError(
|
|
522
|
+
error: unknown,
|
|
523
|
+
provider: string,
|
|
524
|
+
model: string,
|
|
525
|
+
): ProviderResponseError {
|
|
512
526
|
const message = error instanceof Error ? error.message : String(error);
|
|
513
527
|
const sanitized = message
|
|
514
528
|
.replace(
|
|
@@ -516,5 +530,10 @@ function sanitizedProviderError(error: unknown): Error {
|
|
|
516
530
|
"[redacted image data]",
|
|
517
531
|
)
|
|
518
532
|
.replace(/Bearer\s+[A-Za-z0-9._~+/-]+/giu, "Bearer [redacted]");
|
|
519
|
-
return new
|
|
533
|
+
return new ProviderResponseError(
|
|
534
|
+
"provider_request_error",
|
|
535
|
+
sanitized,
|
|
536
|
+
{ provider, model },
|
|
537
|
+
{ cause: error },
|
|
538
|
+
);
|
|
520
539
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ProviderResponseError } from "./model-client";
|
|
2
|
+
|
|
1
3
|
type ProviderContext = { provider: string; model: string };
|
|
2
4
|
|
|
3
5
|
type ToolCallAccumulator = {
|
|
@@ -265,8 +267,10 @@ function providerStreamError(
|
|
|
265
267
|
options: ProviderContext,
|
|
266
268
|
path: string,
|
|
267
269
|
detail: string,
|
|
268
|
-
):
|
|
269
|
-
return new
|
|
270
|
+
): ProviderResponseError {
|
|
271
|
+
return new ProviderResponseError(
|
|
272
|
+
"invalid_provider_stream",
|
|
270
273
|
`Invalid provider stream (provider=${options.provider}, model=${options.model}): ${path} ${detail}.`,
|
|
274
|
+
{ provider: options.provider, model: options.model, path },
|
|
271
275
|
);
|
|
272
276
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { buildOutputSnapshotFromText } from "./task-output-snapshot";
|
|
|
11
11
|
import { defineToolExecutor } from "./types";
|
|
12
12
|
import type { TaskOutputSnapshot } from "./task-output";
|
|
13
13
|
import type { BashRawResult, ToolExecutionContext, ToolExecutor } from "./types";
|
|
14
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
|
|
14
15
|
|
|
15
16
|
type BashArgs = {
|
|
16
17
|
command: string;
|
|
@@ -27,25 +28,16 @@ export type BashToolOptions = {
|
|
|
27
28
|
maxTimeoutMs?: number;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
const defaultForegroundTimeoutMs = 5_000;
|
|
31
|
-
const defaultMaxForegroundTimeoutMs = 600_000;
|
|
32
|
-
|
|
33
31
|
export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
|
|
34
32
|
const maxTimeoutMs =
|
|
35
|
-
options.maxTimeoutMs ??
|
|
36
|
-
parsePositiveInteger(
|
|
37
|
-
process.env.TINKER_BASH_MAX_TIMEOUT_MS,
|
|
38
|
-
defaultMaxForegroundTimeoutMs,
|
|
39
|
-
);
|
|
33
|
+
options.maxTimeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.bashMaxTimeoutMs;
|
|
40
34
|
const defaultTimeoutMs =
|
|
41
|
-
options.defaultTimeoutMs ??
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
defaultForegroundTimeoutMs,
|
|
46
|
-
),
|
|
47
|
-
maxTimeoutMs,
|
|
35
|
+
options.defaultTimeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.bashDefaultTimeoutMs;
|
|
36
|
+
if (defaultTimeoutMs > maxTimeoutMs) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Bash default timeout must not exceed max timeout; received ${defaultTimeoutMs} > ${maxTimeoutMs}.`,
|
|
48
39
|
);
|
|
40
|
+
}
|
|
49
41
|
|
|
50
42
|
return defineToolExecutor("bash", {
|
|
51
43
|
definition: {
|
|
@@ -161,7 +153,7 @@ export function createBashToolExecutor(options: BashToolOptions): ToolExecutor {
|
|
|
161
153
|
|
|
162
154
|
export function parseBashArgs(
|
|
163
155
|
args: unknown,
|
|
164
|
-
maxTimeoutMs =
|
|
156
|
+
maxTimeoutMs = DEFAULT_PUBLIC_TOOLING_CONFIG.bashMaxTimeoutMs,
|
|
165
157
|
): { ok: true; value: BashArgs } | { ok: false; error: string } {
|
|
166
158
|
if (!isRecord(args)) {
|
|
167
159
|
return { ok: false, error: "Bash arguments must be an object." };
|
|
@@ -401,15 +393,6 @@ function parseOptionalTimeout(
|
|
|
401
393
|
return { ok: true, value };
|
|
402
394
|
}
|
|
403
395
|
|
|
404
|
-
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
405
|
-
if (value === undefined) {
|
|
406
|
-
return fallback;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const parsed = Number(value);
|
|
410
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
396
|
function lastPipelineCommandName(command: string): string | undefined {
|
|
414
397
|
const segments = command.split("|");
|
|
415
398
|
const lastSegment = segments.at(-1)?.trim();
|
package/src/tools/grep.ts
CHANGED
|
@@ -32,6 +32,11 @@ type GrepArgs = {
|
|
|
32
32
|
export type GrepToolOptions = {
|
|
33
33
|
workspaceRoot: string;
|
|
34
34
|
cwdState: CwdState;
|
|
35
|
+
ripgrep?: {
|
|
36
|
+
command?: string;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
maxBufferBytes?: number;
|
|
39
|
+
};
|
|
35
40
|
};
|
|
36
41
|
|
|
37
42
|
const ignoredDirectories = [
|
|
@@ -174,7 +179,10 @@ export function createGrepToolExecutor(options: GrepToolOptions): ToolExecutor {
|
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
const rgArgs = buildRipgrepArgs(input, mode, absoluteSearchPath);
|
|
177
|
-
const rg = await ripGrep(rgArgs, {
|
|
182
|
+
const rg = await ripGrep(rgArgs, {
|
|
183
|
+
signal: context.signal,
|
|
184
|
+
...options.ripgrep,
|
|
185
|
+
});
|
|
178
186
|
|
|
179
187
|
if (!rg.ok) {
|
|
180
188
|
return grepFailure({
|
package/src/tools/registry.ts
CHANGED
|
@@ -31,6 +31,10 @@ import { ToolExecutionFatalError } from "./types";
|
|
|
31
31
|
import type { SkillCatalogSnapshot } from "../skills/skill-loader";
|
|
32
32
|
import type { SkillActivationCoordinator } from "../skills/skill-context";
|
|
33
33
|
import { createSkillToolExecutor } from "../skills/skill-tool";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_PUBLIC_TOOLING_CONFIG,
|
|
36
|
+
type PublicToolingConfig,
|
|
37
|
+
} from "../cli/public-config-contract";
|
|
34
38
|
|
|
35
39
|
export class ToolRegistry {
|
|
36
40
|
private readonly tools = new Map<string, ToolExecutor>();
|
|
@@ -128,10 +132,12 @@ export function createDefaultTooling(options: {
|
|
|
128
132
|
taskStopGraceMs?: number;
|
|
129
133
|
skillCatalog?: SkillCatalogSnapshot;
|
|
130
134
|
skillCoordinator?: SkillActivationCoordinator;
|
|
135
|
+
toolingConfig?: PublicToolingConfig;
|
|
131
136
|
}): DefaultTooling {
|
|
132
137
|
const snapshots: FileSnapshotStore = new Map();
|
|
133
138
|
const registry = new ToolRegistry();
|
|
134
139
|
const runtimeSession = options.runtimeSession;
|
|
140
|
+
const toolingConfig = options.toolingConfig ?? DEFAULT_PUBLIC_TOOLING_CONFIG;
|
|
135
141
|
const cwdState = createCwdState(options.workspaceRoot);
|
|
136
142
|
const taskManager = new ShellTaskManager({
|
|
137
143
|
workspaceRoot: options.workspaceRoot,
|
|
@@ -149,6 +155,11 @@ export function createDefaultTooling(options: {
|
|
|
149
155
|
createGrepToolExecutor({
|
|
150
156
|
workspaceRoot: options.workspaceRoot,
|
|
151
157
|
cwdState,
|
|
158
|
+
ripgrep: {
|
|
159
|
+
command: toolingConfig.ripgrepPath,
|
|
160
|
+
timeoutMs: toolingConfig.grepTimeoutMs,
|
|
161
|
+
maxBufferBytes: toolingConfig.grepMaxBufferBytes,
|
|
162
|
+
},
|
|
152
163
|
}),
|
|
153
164
|
);
|
|
154
165
|
registry.register(
|
|
@@ -192,13 +203,15 @@ export function createDefaultTooling(options: {
|
|
|
192
203
|
workspaceRoot: options.workspaceRoot,
|
|
193
204
|
cwdState,
|
|
194
205
|
taskManager,
|
|
206
|
+
defaultTimeoutMs: toolingConfig.bashDefaultTimeoutMs,
|
|
207
|
+
maxTimeoutMs: toolingConfig.bashMaxTimeoutMs,
|
|
195
208
|
}),
|
|
196
209
|
);
|
|
197
210
|
registry.register(createTaskListToolExecutor({ taskManager }));
|
|
198
211
|
registry.register(createTaskOutputToolExecutor({ taskManager }));
|
|
199
212
|
registry.register(createTaskStopToolExecutor({ taskManager }));
|
|
200
213
|
|
|
201
|
-
const exaApiKey = options.exaApiKey ??
|
|
214
|
+
const exaApiKey = options.exaApiKey ?? toolingConfig.exaApiKey;
|
|
202
215
|
const hasExaKey = exaApiKey !== undefined && exaApiKey.trim() !== "";
|
|
203
216
|
|
|
204
217
|
if (hasExaKey) {
|
|
@@ -209,6 +222,7 @@ export function createDefaultTooling(options: {
|
|
|
209
222
|
createWebFetchToolExecutor({
|
|
210
223
|
exaApiKey: hasExaKey ? exaApiKey : undefined,
|
|
211
224
|
refiner: options.webFetchRefiner,
|
|
225
|
+
refineThreshold: toolingConfig.webFetchRefineThreshold,
|
|
212
226
|
}),
|
|
213
227
|
);
|
|
214
228
|
|
package/src/tools/ripgrep.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { rgPath } from "@vscode/ripgrep";
|
|
3
2
|
import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
3
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
|
|
4
4
|
|
|
5
5
|
export const RIPGREP_MISSING_ERROR =
|
|
6
6
|
"Tinker's bundled ripgrep executable is unavailable. Reinstall tinker-agent.";
|
|
7
7
|
|
|
8
|
-
const defaultTimeoutMs = 20_000;
|
|
9
|
-
const defaultMaxBufferBytes = 20_000_000;
|
|
10
|
-
|
|
11
8
|
export type RipgrepResult = {
|
|
12
9
|
ok: boolean;
|
|
13
10
|
lines: string[];
|
|
@@ -18,12 +15,13 @@ export type RipgrepResult = {
|
|
|
18
15
|
|
|
19
16
|
export type RipgrepOptions = {
|
|
20
17
|
signal: AbortSignal;
|
|
18
|
+
command?: string;
|
|
21
19
|
timeoutMs?: number;
|
|
22
20
|
maxBufferBytes?: number;
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
export function findRipgrepCommand(): string {
|
|
26
|
-
return
|
|
23
|
+
export function findRipgrepCommand(command?: string): string {
|
|
24
|
+
return command ?? DEFAULT_PUBLIC_TOOLING_CONFIG.ripgrepPath;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
export async function ripGrep(
|
|
@@ -31,21 +29,28 @@ export async function ripGrep(
|
|
|
31
29
|
options: RipgrepOptions,
|
|
32
30
|
): Promise<RipgrepResult> {
|
|
33
31
|
throwIfTurnCancelled(options.signal);
|
|
34
|
-
const timeoutMs =
|
|
35
|
-
options.timeoutMs ??
|
|
36
|
-
parsePositiveInteger(process.env.TINKER_GREP_TIMEOUT_MS, defaultTimeoutMs);
|
|
32
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepTimeoutMs;
|
|
37
33
|
const maxBufferBytes =
|
|
38
|
-
options.maxBufferBytes ??
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
options.maxBufferBytes ?? DEFAULT_PUBLIC_TOOLING_CONFIG.grepMaxBufferBytes;
|
|
35
|
+
const command = findRipgrepCommand(options.command);
|
|
36
|
+
|
|
37
|
+
const first = await runRipgrep(
|
|
38
|
+
command,
|
|
39
|
+
args,
|
|
40
|
+
timeoutMs,
|
|
41
|
+
maxBufferBytes,
|
|
42
|
+
options.signal,
|
|
43
|
+
);
|
|
45
44
|
if (first.retryWithSingleThread) {
|
|
46
45
|
throwIfTurnCancelled(options.signal);
|
|
47
46
|
return finalizeResult(
|
|
48
|
-
await runRipgrep(
|
|
47
|
+
await runRipgrep(
|
|
48
|
+
command,
|
|
49
|
+
["-j", "1", ...args],
|
|
50
|
+
timeoutMs,
|
|
51
|
+
maxBufferBytes,
|
|
52
|
+
options.signal,
|
|
53
|
+
),
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -62,6 +67,7 @@ type RipgrepAttempt = {
|
|
|
62
67
|
};
|
|
63
68
|
|
|
64
69
|
function runRipgrep(
|
|
70
|
+
command: string,
|
|
65
71
|
args: string[],
|
|
66
72
|
timeoutMs: number,
|
|
67
73
|
maxBufferBytes: number,
|
|
@@ -74,7 +80,7 @@ function runRipgrep(
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
execFile(
|
|
77
|
-
|
|
83
|
+
command,
|
|
78
84
|
args,
|
|
79
85
|
{ timeout: timeoutMs, maxBuffer: maxBufferBytes, signal },
|
|
80
86
|
(error, stdout, stderr) => {
|
|
@@ -210,12 +216,3 @@ function isEagainError(
|
|
|
210
216
|
stderr.includes("Resource temporarily unavailable")
|
|
211
217
|
);
|
|
212
218
|
}
|
|
213
|
-
|
|
214
|
-
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
215
|
-
if (value === undefined || value.trim() === "") {
|
|
216
|
-
return fallback;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const parsed = Number(value);
|
|
220
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
221
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cancellationError, throwIfTurnCancelled } from "../../agent/turn-cancellation";
|
|
2
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../../cli/public-config-contract";
|
|
2
3
|
import { defineToolExecutor } from "../types";
|
|
3
4
|
import type { ToolExecutionContext, ToolExecutor, WebFetchRawResult } from "../types";
|
|
4
5
|
import type { WebFetchBackend, WebFetchBackendResult, WebFetchRoute } from "./backend";
|
|
@@ -25,7 +26,6 @@ export type WebFetchToolOptions = {
|
|
|
25
26
|
browserBackend?: WebFetchBackend | false;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
export const WEB_FETCH_DEFAULT_REFINE_THRESHOLD = 2000;
|
|
29
29
|
export const WEB_FETCH_DEFAULT_CACHE_TTL_MS = 15 * 60 * 1000;
|
|
30
30
|
|
|
31
31
|
type CacheEntry = {
|
|
@@ -50,11 +50,7 @@ export function createWebFetchToolExecutor(
|
|
|
50
50
|
: (options.browserBackend ??
|
|
51
51
|
(isBrowserBackendAvailable() ? createBrowserWebFetchBackend() : undefined));
|
|
52
52
|
const refineThreshold =
|
|
53
|
-
options.refineThreshold ??
|
|
54
|
-
parsePositiveInteger(
|
|
55
|
-
process.env.TINKER_WEBFETCH_REFINE_THRESHOLD,
|
|
56
|
-
WEB_FETCH_DEFAULT_REFINE_THRESHOLD,
|
|
57
|
-
);
|
|
53
|
+
options.refineThreshold ?? DEFAULT_PUBLIC_TOOLING_CONFIG.webFetchRefineThreshold;
|
|
58
54
|
const cacheTtlMs = options.cacheTtlMs ?? WEB_FETCH_DEFAULT_CACHE_TTL_MS;
|
|
59
55
|
const cache = new Map<string, CacheEntry>();
|
|
60
56
|
|
|
@@ -284,15 +280,6 @@ function pruneCache(cache: Map<string, CacheEntry>): void {
|
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
|
288
|
-
if (value === undefined || value.trim() === "") {
|
|
289
|
-
return fallback;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const parsed = Number(value);
|
|
293
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
283
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
297
284
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
298
285
|
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -42,12 +42,14 @@ import type { SessionSummary } from "../session/session-catalog";
|
|
|
42
42
|
import type { ModelProfile, ModelProfiles } from "../cli/model-profiles";
|
|
43
43
|
import { loadViewFile, type ViewFile } from "./view-file";
|
|
44
44
|
import { writeClipboardText } from "./clipboard";
|
|
45
|
+
import type { WorkspaceFileLister } from "./workspace-file-search";
|
|
45
46
|
|
|
46
47
|
export type AppProps = {
|
|
47
48
|
sessionController: TuiSessionController;
|
|
48
49
|
readGitBranch?: (workspaceRoot: string) => Promise<string | undefined>;
|
|
49
50
|
history?: PromptHistory;
|
|
50
51
|
projectSlashCommands?: readonly ProjectSlashCommand[];
|
|
52
|
+
fileLister?: WorkspaceFileLister;
|
|
51
53
|
profiles?: ModelProfiles;
|
|
52
54
|
persistDefaultProfile?: (profileName: string) => Promise<void>;
|
|
53
55
|
readViewFile?: (workspaceRoot: string, filePath: string) => Promise<ViewFile>;
|
|
@@ -647,6 +649,7 @@ export function App(props: AppProps) {
|
|
|
647
649
|
isDisabled={isRunning || isSessionOperation || isCopying}
|
|
648
650
|
history={props.history}
|
|
649
651
|
commands={availableCommands}
|
|
652
|
+
fileLister={props.fileLister}
|
|
650
653
|
importImage={binding.importImage}
|
|
651
654
|
verifyImageAssets={binding.verifyImageAssets}
|
|
652
655
|
onSubmit={onSubmit}
|
|
@@ -222,7 +222,8 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
222
222
|
if (mention === undefined || locked) {
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
const importImage = props.importImage;
|
|
226
|
+
if (importImage === undefined) {
|
|
226
227
|
insertFilePath(filePath);
|
|
227
228
|
return;
|
|
228
229
|
}
|
|
@@ -236,8 +237,10 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
236
237
|
phase: { kind: "attaching", operationId },
|
|
237
238
|
error: undefined,
|
|
238
239
|
}));
|
|
239
|
-
void
|
|
240
|
-
.
|
|
240
|
+
void Promise.resolve()
|
|
241
|
+
.then(() =>
|
|
242
|
+
importImage(filePath, controller.signal, captured.attachments.length + 1),
|
|
243
|
+
)
|
|
241
244
|
.then((imported) => {
|
|
242
245
|
setState((current) => {
|
|
243
246
|
if (
|
package/src/tui/event-store.ts
CHANGED
|
@@ -153,13 +153,21 @@ export function reduceTuiProjection(
|
|
|
153
153
|
}
|
|
154
154
|
case "model.request.started":
|
|
155
155
|
return updateActiveTurn(state, event, policy, (turn) =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
event.data.attemptNumber === 1
|
|
157
|
+
? appendTurnItem(turn, {
|
|
158
|
+
id: `model-${event.iterationId}`,
|
|
159
|
+
ref: modelRequestRef(event.iterationId),
|
|
160
|
+
text: `model iteration ${event.iterationNumber}`,
|
|
161
|
+
status: "running",
|
|
162
|
+
})
|
|
163
|
+
: updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
164
|
+
...item,
|
|
165
|
+
text: `model iteration ${event.iterationNumber} · retrying`,
|
|
166
|
+
status: "running",
|
|
167
|
+
})),
|
|
162
168
|
);
|
|
169
|
+
case "model.request.failed":
|
|
170
|
+
return state;
|
|
163
171
|
case "model.request.finished":
|
|
164
172
|
return updateActiveTurn(state, event, policy, (turn) =>
|
|
165
173
|
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
@@ -333,7 +341,7 @@ export function reduceTuiProjection(
|
|
|
333
341
|
}
|
|
334
342
|
case "turn.failed": {
|
|
335
343
|
const active = requireActiveTurn(state, event);
|
|
336
|
-
const failed = appendTurnItem(active, {
|
|
344
|
+
const failed = appendTurnItem(markRunningItemsFailed(active), {
|
|
337
345
|
id: `turn-${active.turnId}-failed-${event.eventSequence}`,
|
|
338
346
|
label: "error",
|
|
339
347
|
text: event.data.error,
|
|
@@ -467,6 +475,15 @@ function appendTurnItem(
|
|
|
467
475
|
return { ...turn, items: [...turn.items, item] };
|
|
468
476
|
}
|
|
469
477
|
|
|
478
|
+
function markRunningItemsFailed(turn: TuiTurnProjection): TuiTurnProjection {
|
|
479
|
+
return {
|
|
480
|
+
...turn,
|
|
481
|
+
items: turn.items.map((item) =>
|
|
482
|
+
item.status === "running" ? { ...item, status: "failed" } : item,
|
|
483
|
+
),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
470
487
|
function updateTurnItem(
|
|
471
488
|
turn: TuiTurnProjection,
|
|
472
489
|
ref: string,
|