tinker-agent 2.10.0 → 2.12.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 +47 -1
- package/README.md +44 -67
- package/package.json +4 -3
- package/src/agent/loop.ts +50 -13
- package/src/agent/runtime-provider-retry.ts +115 -0
- package/src/agent/runtime-session-contracts.ts +13 -29
- package/src/agent/runtime-session.ts +42 -67
- package/src/cli/config.ts +0 -29
- package/src/cli/model-profiles.ts +0 -80
- package/src/cli/public-config-contract.ts +1 -82
- package/src/cli/tui-runner.tsx +3 -41
- package/src/events/observation-text-log.ts +7 -0
- package/src/events/types.ts +8 -0
- package/src/image/abortable-file-open.ts +54 -0
- package/src/image/image-asset-store.ts +7 -1
- package/src/memory/contracts.ts +0 -256
- package/src/memory/memory-create-tool.ts +3 -5
- package/src/memory/memory-files.ts +145 -0
- package/src/memory/memory-search-output.ts +23 -0
- package/src/memory/memory-search-tool.ts +79 -175
- package/src/memory/memory-search.ts +115 -0
- package/src/model/fake-model-client.ts +20 -1
- package/src/model/openai-model-utils.ts +45 -1
- package/src/model/openai-responses-mapping.ts +11 -0
- package/src/model/openai-responses-stream.ts +14 -0
- package/src/observation/observation-builder.ts +5 -1
- package/src/session/scoped-query-database.ts +27 -0
- package/src/session/session-history-access.ts +4 -3
- package/src/session/session-store-contracts.ts +0 -23
- package/src/session/session-store.ts +9 -106
- package/src/tools/grep.ts +12 -4
- package/src/tools/registry.ts +15 -18
- package/src/tools/types.ts +12 -0
- package/src/tui/app.tsx +51 -7
- package/src/tui/components/ask-user.tsx +15 -8
- package/src/tui/components/prompt-input.tsx +14 -6
- package/src/tui/components/timeline.tsx +17 -9
- package/src/tui/event-store.ts +13 -1
- package/src/tui/file-mention.ts +29 -5
- package/src/tui/tui-projection-store.ts +5 -2
- package/src/tui/tui-session-controller.ts +8 -0
- package/src/tui/workspace-file-search.ts +21 -0
- package/src/cli/tui-memory.ts +0 -69
- package/src/memory/embedding-client.ts +0 -105
- package/src/memory/memory-coordinator.ts +0 -1274
- package/src/memory/memory-delete-tool.ts +0 -88
- package/src/memory/memory-extractor.ts +0 -252
- package/src/memory/memory-get-tool.ts +0 -86
- package/src/memory/memory-log.ts +0 -88
- package/src/memory/memory-store.ts +0 -1133
- package/src/memory/memory-update-tool.ts +0 -142
- package/src/memory/vector.ts +0 -153
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { prepareSessionMemory } from "../memory/memory-files";
|
|
1
2
|
import { Database } from "bun:sqlite";
|
|
3
|
+
import { ScopedQueryDatabase } from "./scoped-query-database";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { chmod, mkdir, open, readdir, rename, rmdir } from "node:fs/promises";
|
|
4
6
|
import path from "node:path";
|
|
@@ -87,8 +89,6 @@ import {
|
|
|
87
89
|
type CommitSurfaceRefreshOptions,
|
|
88
90
|
type CommitSwapRevisionInput,
|
|
89
91
|
type CommitSwapRevisionOptions,
|
|
90
|
-
type CompletedTurnMessageSnapshot,
|
|
91
|
-
type CompletedTurnSnapshot,
|
|
92
92
|
type CreateNewSessionStoreInput,
|
|
93
93
|
type OpenSessionStoreInput,
|
|
94
94
|
type SessionCloseReason,
|
|
@@ -131,9 +131,7 @@ import { requireItem, requireSingleChange, runTransaction } from "./session-stor
|
|
|
131
131
|
import { SessionStoreValidation } from "./session-store-validation";
|
|
132
132
|
import {
|
|
133
133
|
assertMeasuredContextAnchor,
|
|
134
|
-
enumFromSql,
|
|
135
134
|
nullableStringFromSql,
|
|
136
|
-
nullableTextFromSql,
|
|
137
135
|
numberFromSql,
|
|
138
136
|
recordFromSql,
|
|
139
137
|
stringFromSql,
|
|
@@ -885,105 +883,6 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
885
883
|
});
|
|
886
884
|
}
|
|
887
885
|
|
|
888
|
-
readCompletedTurnSnapshot(turnId: TurnId): CompletedTurnSnapshot {
|
|
889
|
-
this.requireOpen();
|
|
890
|
-
const turnRow = this.database
|
|
891
|
-
.query("SELECT status FROM turns WHERE turn_id = ?")
|
|
892
|
-
.get(turnId);
|
|
893
|
-
const status = enumFromSql(
|
|
894
|
-
recordFromSql(turnRow, "completed turn").status,
|
|
895
|
-
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
896
|
-
"turn status",
|
|
897
|
-
);
|
|
898
|
-
if (status !== "completed") {
|
|
899
|
-
throw new Error(`Turn ${turnId} is not completed.`);
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const rows = this.database
|
|
903
|
-
.query(
|
|
904
|
-
`SELECT ordinal, role, content, reasoning_content,
|
|
905
|
-
reasoning_content_present, name
|
|
906
|
-
FROM messages
|
|
907
|
-
WHERE turn_id = ?
|
|
908
|
-
ORDER BY ordinal`,
|
|
909
|
-
)
|
|
910
|
-
.all(turnId);
|
|
911
|
-
if (rows.length === 0) {
|
|
912
|
-
throw new Error(`Completed turn ${turnId} has no messages.`);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
let previousOrdinal = 0;
|
|
916
|
-
const messages = rows.map((value): CompletedTurnMessageSnapshot => {
|
|
917
|
-
const row = recordFromSql(value, "completed turn message");
|
|
918
|
-
const ordinal = numberFromSql(row.ordinal, "completed turn ordinal");
|
|
919
|
-
if (ordinal < 1 || ordinal <= previousOrdinal) {
|
|
920
|
-
throw new Error("Completed turn message ordinals are invalid.");
|
|
921
|
-
}
|
|
922
|
-
previousOrdinal = ordinal;
|
|
923
|
-
const role = enumFromSql(
|
|
924
|
-
row.role,
|
|
925
|
-
["user", "assistant", "tool"] as const,
|
|
926
|
-
"completed turn message role",
|
|
927
|
-
);
|
|
928
|
-
if (role === "user") {
|
|
929
|
-
if (
|
|
930
|
-
row.reasoning_content !== null ||
|
|
931
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !==
|
|
932
|
-
0 ||
|
|
933
|
-
row.name !== null
|
|
934
|
-
) {
|
|
935
|
-
throw new Error("Completed user message fields are invalid.");
|
|
936
|
-
}
|
|
937
|
-
return Object.freeze({
|
|
938
|
-
ordinal,
|
|
939
|
-
role,
|
|
940
|
-
content: stringFromSql(row.content, "completed user content"),
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
if (role === "assistant") {
|
|
944
|
-
if (row.name !== null) {
|
|
945
|
-
throw new Error("Completed assistant message name must be null.");
|
|
946
|
-
}
|
|
947
|
-
const reasoningPresent = numberFromSql(
|
|
948
|
-
row.reasoning_content_present,
|
|
949
|
-
"reasoning_content_present",
|
|
950
|
-
);
|
|
951
|
-
if (reasoningPresent !== 0 && reasoningPresent !== 1) {
|
|
952
|
-
throw new Error("reasoning_content_present must be 0 or 1.");
|
|
953
|
-
}
|
|
954
|
-
if (reasoningPresent === 0 && row.reasoning_content !== null) {
|
|
955
|
-
throw new Error("Absent assistant reasoning content must be null.");
|
|
956
|
-
}
|
|
957
|
-
return Object.freeze({
|
|
958
|
-
ordinal,
|
|
959
|
-
role,
|
|
960
|
-
content: nullableTextFromSql(row.content, "completed assistant content"),
|
|
961
|
-
...(reasoningPresent === 0
|
|
962
|
-
? {}
|
|
963
|
-
: {
|
|
964
|
-
reasoningContent: nullableTextFromSql(
|
|
965
|
-
row.reasoning_content,
|
|
966
|
-
"completed assistant reasoning content",
|
|
967
|
-
),
|
|
968
|
-
}),
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
if (
|
|
972
|
-
row.reasoning_content !== null ||
|
|
973
|
-
numberFromSql(row.reasoning_content_present, "reasoning_content_present") !== 0
|
|
974
|
-
) {
|
|
975
|
-
throw new Error("Completed tool message reasoning fields are invalid.");
|
|
976
|
-
}
|
|
977
|
-
return Object.freeze({
|
|
978
|
-
ordinal,
|
|
979
|
-
role,
|
|
980
|
-
name: stringFromSql(row.name, "completed tool name"),
|
|
981
|
-
content: stringFromSql(row.content, "completed tool content"),
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
return Object.freeze({ messages: Object.freeze(messages) });
|
|
985
|
-
}
|
|
986
|
-
|
|
987
886
|
loadProtocolView(): ProtocolContextView {
|
|
988
887
|
this.requireOpen();
|
|
989
888
|
const imageAttachments = loadMessageImageAttachments(this.database);
|
|
@@ -1327,7 +1226,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1327
1226
|
await chmod(stagingDatabasePath, 0o600);
|
|
1328
1227
|
input.faultInjector?.("after_snapshot");
|
|
1329
1228
|
|
|
1330
|
-
stagingDatabase = openWritableDatabase(stagingDatabasePath);
|
|
1229
|
+
stagingDatabase = openWritableDatabase(stagingDatabasePath, ScopedQueryDatabase);
|
|
1331
1230
|
verifySessionSchema(stagingDatabase, this.sessionId);
|
|
1332
1231
|
dropSessionCloneTriggers(stagingDatabase);
|
|
1333
1232
|
input.faultInjector?.("after_trigger_drop");
|
|
@@ -1403,6 +1302,7 @@ export class SessionStore implements SessionLedgerCommitter {
|
|
|
1403
1302
|
input.faultInjector?.("before_publish_rename");
|
|
1404
1303
|
await rename(stagingDirectory, targetDirectory);
|
|
1405
1304
|
published = true;
|
|
1305
|
+
await prepareSessionMemory(targetDirectory, input.targetSessionId, this.homeRoot);
|
|
1406
1306
|
} finally {
|
|
1407
1307
|
if (stagingDatabase !== undefined) {
|
|
1408
1308
|
try {
|
|
@@ -1542,8 +1442,11 @@ export async function resolveSessionDatabasePath(
|
|
|
1542
1442
|
);
|
|
1543
1443
|
}
|
|
1544
1444
|
|
|
1545
|
-
function openWritableDatabase(
|
|
1546
|
-
|
|
1445
|
+
function openWritableDatabase(
|
|
1446
|
+
databasePath: string,
|
|
1447
|
+
DatabaseType: typeof Database = Database,
|
|
1448
|
+
): Database {
|
|
1449
|
+
const database = new DatabaseType(databasePath, {
|
|
1547
1450
|
create: false,
|
|
1548
1451
|
readwrite: true,
|
|
1549
1452
|
strict: true,
|
package/src/tools/grep.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function createGrepToolExecutor(options: GrepToolOptions): ToolExecutor {
|
|
|
72
72
|
path: {
|
|
73
73
|
type: "string",
|
|
74
74
|
description:
|
|
75
|
-
"Optional workspace-relative or absolute file or directory to search in. Defaults to the current workspace-local cwd. For directories, ripgrep runs in that directory with . as its search path; explicitly selecting an excluded directory allows searching inside it. Files are passed as absolute paths: explicit files bypass ignore/glob/type filtering, and explicit symlink files are followed. Explicit binary files may yield matches but are not guaranteed to be searched completely.",
|
|
75
|
+
"Optional workspace-relative or absolute file or directory to search in. Defaults to the current workspace-local cwd. For directories, ripgrep runs in that directory with . as its search path; explicitly selecting an excluded directory allows searching inside it. Files are passed as absolute paths: explicit files bypass ignore/glob/type filtering, and explicit symlink files are followed. Explicit binary files may yield matches but are not guaranteed to be searched completely, and reported line numbers may be inaccurate.",
|
|
76
76
|
},
|
|
77
77
|
glob: {
|
|
78
78
|
type: "string",
|
|
@@ -202,7 +202,7 @@ export function createGrepToolExecutor(options: GrepToolOptions): ToolExecutor {
|
|
|
202
202
|
absoluteSearchPath,
|
|
203
203
|
mode,
|
|
204
204
|
truncated: rg.truncated ? true : undefined,
|
|
205
|
-
error: rg.error ?? "ripgrep failed.",
|
|
205
|
+
error: omitUnsupportedRegexHint(rg.error ?? "ripgrep failed."),
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
|
|
@@ -368,11 +368,19 @@ export function buildRipgrepArgs(
|
|
|
368
368
|
function resolveGrepContext(input: GrepArgs) {
|
|
369
369
|
const both = input.context ?? input.contextAlias;
|
|
370
370
|
return {
|
|
371
|
-
before:
|
|
372
|
-
after:
|
|
371
|
+
before: input.before ?? both ?? 0,
|
|
372
|
+
after: input.after ?? both ?? 0,
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
function omitUnsupportedRegexHint(error: string): string {
|
|
377
|
+
// Grep does not expose rg's PCRE2 flag; preserve the diagnostic itself.
|
|
378
|
+
return error.replace(
|
|
379
|
+
/\n+Consider enabling PCRE2 with the --pcre2 flag, which can handle backreferences\s+and look-around\.\s*$/,
|
|
380
|
+
"",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
376
384
|
type ParsedGrepArgs =
|
|
377
385
|
| { ok: true; value: GrepArgs }
|
|
378
386
|
| { ok: false; error: string; pattern?: string; mode?: GrepOutputMode };
|
package/src/tools/registry.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createMemorySearchToolExecutor } from "../memory/memory-search-tool";
|
|
2
|
+
import { createFileMemoryCreateToolExecutor } from "../memory/memory-files";
|
|
1
3
|
import { createAskUserToolExecutor } from "./ask-user";
|
|
2
4
|
import { createBashToolExecutor } from "./bash";
|
|
3
5
|
import { ShellTaskManager } from "./bash-task";
|
|
@@ -182,10 +184,7 @@ export function createDefaultTooling(options: {
|
|
|
182
184
|
skillCoordinator?: SkillActivationCoordinator;
|
|
183
185
|
toolingConfig?: PublicToolingConfig;
|
|
184
186
|
memorySearch?: ToolExecutor;
|
|
185
|
-
memoryGet?: ToolExecutor;
|
|
186
187
|
memoryCreate?: ToolExecutor;
|
|
187
|
-
memoryUpdate?: ToolExecutor;
|
|
188
|
-
memoryDelete?: ToolExecutor;
|
|
189
188
|
enableTurnUndo?: boolean;
|
|
190
189
|
imageAssetStore?: ImageAssetStore;
|
|
191
190
|
supportsViewImage?: boolean;
|
|
@@ -259,21 +258,19 @@ export function createDefaultTooling(options: {
|
|
|
259
258
|
registry.register(createContextStatusToolExecutor());
|
|
260
259
|
registry.register(createContextSwapCandidatesToolExecutor());
|
|
261
260
|
registry.register(createContextSwapToolExecutor());
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
registry.register(options.memoryDelete);
|
|
276
|
-
}
|
|
261
|
+
registry.register(
|
|
262
|
+
options.memorySearch ??
|
|
263
|
+
createMemorySearchToolExecutor({
|
|
264
|
+
homeRoot: options.homeRoot,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
registry.register(
|
|
268
|
+
options.memoryCreate ??
|
|
269
|
+
createFileMemoryCreateToolExecutor({
|
|
270
|
+
workspaceRoot: options.workspaceRoot,
|
|
271
|
+
homeRoot: options.homeRoot,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
277
274
|
if (options.skillCatalog !== undefined) {
|
|
278
275
|
if (options.skillCatalog.skills.size === 0) {
|
|
279
276
|
throw new Error("An empty Agent Skill catalog must not register tooling.");
|
package/src/tools/types.ts
CHANGED
|
@@ -427,7 +427,18 @@ export type ContextMaintenanceHandle = {
|
|
|
427
427
|
): Promise<ContextSwapRawResult>;
|
|
428
428
|
};
|
|
429
429
|
|
|
430
|
+
export type MemorySearchLine = { lineNumber: number; match: boolean; text: string };
|
|
431
|
+
export type MemoryTextSearchResult = {
|
|
432
|
+
ok: true;
|
|
433
|
+
format: "text";
|
|
434
|
+
files: readonly { filePath: string; lines: readonly MemorySearchLine[] }[];
|
|
435
|
+
returnedResults: number;
|
|
436
|
+
hasMore: boolean;
|
|
437
|
+
nextOffset?: number;
|
|
438
|
+
};
|
|
439
|
+
|
|
430
440
|
export type MemorySearchRawResult =
|
|
441
|
+
| MemoryTextSearchResult
|
|
431
442
|
| {
|
|
432
443
|
ok: true;
|
|
433
444
|
degraded: "vector" | "fts" | null;
|
|
@@ -469,6 +480,7 @@ export type MemoryCreateRawResult =
|
|
|
469
480
|
| {
|
|
470
481
|
ok: true;
|
|
471
482
|
status: "created" | "already_exists";
|
|
483
|
+
filePath?: string;
|
|
472
484
|
memoryId: string;
|
|
473
485
|
createdAt: string;
|
|
474
486
|
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EMPTY_PROVIDER_RETRY } from "../agent/runtime-provider-retry";
|
|
1
2
|
import { Box, Static, Text, useApp, useInput, useStdout, useWindowSize } from "ink";
|
|
2
3
|
import {
|
|
3
4
|
useCallback,
|
|
@@ -83,7 +84,9 @@ export type AppProps = {
|
|
|
83
84
|
writeClipboard?: (markdown: string) => Promise<void>;
|
|
84
85
|
onQuit?: () => void;
|
|
85
86
|
initialNotice?: string;
|
|
86
|
-
listStoredMemories?: () =>
|
|
87
|
+
listStoredMemories?: () =>
|
|
88
|
+
| readonly StoredMemorySummary[]
|
|
89
|
+
| Promise<readonly StoredMemorySummary[]>;
|
|
87
90
|
memoryDisabledNotice?: string;
|
|
88
91
|
};
|
|
89
92
|
|
|
@@ -143,6 +146,12 @@ export function App(props: AppProps) {
|
|
|
143
146
|
() => binding.bashGuard(),
|
|
144
147
|
() => binding.bashGuard(),
|
|
145
148
|
);
|
|
149
|
+
const providerRetry = useSyncExternalStore(
|
|
150
|
+
(listener) => binding.subscribeProviderRetry?.(listener) ?? (() => undefined),
|
|
151
|
+
() => binding.providerRetry?.() ?? EMPTY_PROVIDER_RETRY,
|
|
152
|
+
() => EMPTY_PROVIDER_RETRY,
|
|
153
|
+
);
|
|
154
|
+
const pendingProviderRetry = providerRetry.pending;
|
|
146
155
|
const askUser = useSyncExternalStore(
|
|
147
156
|
(listener) => binding.subscribeAskUser(listener),
|
|
148
157
|
() => binding.askUser(),
|
|
@@ -282,7 +291,12 @@ export function App(props: AppProps) {
|
|
|
282
291
|
setIsCancelling(true);
|
|
283
292
|
setNotice("Cancelling current turn...");
|
|
284
293
|
},
|
|
285
|
-
{
|
|
294
|
+
{
|
|
295
|
+
isActive:
|
|
296
|
+
executionRunning &&
|
|
297
|
+
askUser.pending === undefined &&
|
|
298
|
+
pendingProviderRetry === undefined,
|
|
299
|
+
},
|
|
286
300
|
);
|
|
287
301
|
|
|
288
302
|
const closeResumePicker = () => {
|
|
@@ -406,13 +420,13 @@ export function App(props: AppProps) {
|
|
|
406
420
|
restoreStaticViewport();
|
|
407
421
|
};
|
|
408
422
|
|
|
409
|
-
const openMemoryView = () => {
|
|
423
|
+
const openMemoryView = async () => {
|
|
410
424
|
if (props.listStoredMemories === undefined) {
|
|
411
425
|
setNotice(props.memoryDisabledNotice ?? "memory disabled: not configured");
|
|
412
426
|
return;
|
|
413
427
|
}
|
|
414
428
|
try {
|
|
415
|
-
const snapshot = props.listStoredMemories();
|
|
429
|
+
const snapshot = await props.listStoredMemories();
|
|
416
430
|
setNotice(undefined);
|
|
417
431
|
setMemoryView(snapshot);
|
|
418
432
|
} catch (error) {
|
|
@@ -634,7 +648,7 @@ export function App(props: AppProps) {
|
|
|
634
648
|
return true;
|
|
635
649
|
}
|
|
636
650
|
if (command.type === "memory") {
|
|
637
|
-
openMemoryView();
|
|
651
|
+
void openMemoryView();
|
|
638
652
|
return true;
|
|
639
653
|
}
|
|
640
654
|
if (command.type === "copy") {
|
|
@@ -912,7 +926,8 @@ export function App(props: AppProps) {
|
|
|
912
926
|
status={
|
|
913
927
|
isCancelling
|
|
914
928
|
? "cancelling"
|
|
915
|
-
: askUser.pending !== undefined
|
|
929
|
+
: askUser.pending !== undefined ||
|
|
930
|
+
pendingProviderRetry !== undefined
|
|
916
931
|
? "waiting_for_answer"
|
|
917
932
|
: executionRunning
|
|
918
933
|
? "running"
|
|
@@ -924,7 +939,35 @@ export function App(props: AppProps) {
|
|
|
924
939
|
/>
|
|
925
940
|
</Box>
|
|
926
941
|
<Box marginTop={1} flexDirection="column" flexShrink={0}>
|
|
927
|
-
{
|
|
942
|
+
{pendingProviderRetry !== undefined ? (
|
|
943
|
+
<AskUser
|
|
944
|
+
key={pendingProviderRetry.requestId}
|
|
945
|
+
title="Provider request failed"
|
|
946
|
+
question={`Automatic retries exhausted. ${pendingProviderRetry.failure.error.slice(0, 500)}`}
|
|
947
|
+
options={[
|
|
948
|
+
{ description: "Retry again" },
|
|
949
|
+
{ description: "End this turn" },
|
|
950
|
+
]}
|
|
951
|
+
dismissLabel="end this turn"
|
|
952
|
+
onSelect={(index) => {
|
|
953
|
+
void binding
|
|
954
|
+
.resolveProviderRetry?.(
|
|
955
|
+
pendingProviderRetry.requestId,
|
|
956
|
+
index === 0 ? "retry" : "stop",
|
|
957
|
+
)
|
|
958
|
+
.catch((error: unknown) =>
|
|
959
|
+
setNotice(`Retry selection failed: ${errorMessage(error)}`),
|
|
960
|
+
);
|
|
961
|
+
}}
|
|
962
|
+
onDismiss={() => {
|
|
963
|
+
void binding
|
|
964
|
+
.resolveProviderRetry?.(pendingProviderRetry.requestId, "stop")
|
|
965
|
+
.catch((error: unknown) =>
|
|
966
|
+
setNotice(`Retry selection failed: ${errorMessage(error)}`),
|
|
967
|
+
);
|
|
968
|
+
}}
|
|
969
|
+
/>
|
|
970
|
+
) : askUser.pending !== undefined ? (
|
|
928
971
|
<AskUser
|
|
929
972
|
question={askUser.pending.question}
|
|
930
973
|
options={askUser.pending.options}
|
|
@@ -977,6 +1020,7 @@ export function App(props: AppProps) {
|
|
|
977
1020
|
isCopying ||
|
|
978
1021
|
isCancelling ||
|
|
979
1022
|
askUser.pending !== undefined ||
|
|
1023
|
+
pendingProviderRetry !== undefined ||
|
|
980
1024
|
bashGuard.pending !== undefined
|
|
981
1025
|
}
|
|
982
1026
|
history={props.history}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Box, Text, useInput } from "ink";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
export type AskUserProps = {
|
|
5
5
|
question: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
dismissLabel?: string;
|
|
6
8
|
options: readonly { readonly description: string }[];
|
|
7
9
|
onSelect(selectedIndex: number): void;
|
|
8
10
|
onDismiss(): void;
|
|
@@ -10,6 +12,12 @@ export type AskUserProps = {
|
|
|
10
12
|
|
|
11
13
|
export function AskUser(props: AskUserProps) {
|
|
12
14
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
const selection = useRef(0);
|
|
16
|
+
const moveSelection = (offset: number) => {
|
|
17
|
+
selection.current =
|
|
18
|
+
(selection.current + offset + props.options.length) % props.options.length;
|
|
19
|
+
setSelectedIndex(selection.current);
|
|
20
|
+
};
|
|
13
21
|
|
|
14
22
|
useInput((input, key) => {
|
|
15
23
|
if (key.escape) {
|
|
@@ -17,17 +25,15 @@ export function AskUser(props: AskUserProps) {
|
|
|
17
25
|
return;
|
|
18
26
|
}
|
|
19
27
|
if (key.upArrow) {
|
|
20
|
-
|
|
21
|
-
current === 0 ? props.options.length - 1 : current - 1,
|
|
22
|
-
);
|
|
28
|
+
moveSelection(-1);
|
|
23
29
|
return;
|
|
24
30
|
}
|
|
25
31
|
if (key.downArrow) {
|
|
26
|
-
|
|
32
|
+
moveSelection(1);
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
35
|
if (key.return) {
|
|
30
|
-
props.onSelect(
|
|
36
|
+
props.onSelect(selection.current);
|
|
31
37
|
return;
|
|
32
38
|
}
|
|
33
39
|
if (/^[1-6]$/.test(input)) {
|
|
@@ -41,7 +47,7 @@ export function AskUser(props: AskUserProps) {
|
|
|
41
47
|
return (
|
|
42
48
|
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
|
43
49
|
<Text color="cyan" bold>
|
|
44
|
-
Tinker asks
|
|
50
|
+
{props.title ?? "Tinker asks"}
|
|
45
51
|
</Text>
|
|
46
52
|
<Text>{props.question}</Text>
|
|
47
53
|
<Box flexDirection="column" marginTop={1}>
|
|
@@ -54,7 +60,8 @@ export function AskUser(props: AskUserProps) {
|
|
|
54
60
|
))}
|
|
55
61
|
</Box>
|
|
56
62
|
<Text dimColor>
|
|
57
|
-
↑/↓ select · 1-{props.options.length} choose · Enter confirm · Esc
|
|
63
|
+
↑/↓ select · 1-{props.options.length} choose · Enter confirm · Esc{" "}
|
|
64
|
+
{props.dismissLabel ?? "skip"}
|
|
58
65
|
</Text>
|
|
59
66
|
</Box>
|
|
60
67
|
);
|
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
type PromptDraft,
|
|
42
42
|
} from "../prompt-draft";
|
|
43
43
|
import { matchSlashCommands, type SlashCommand } from "../slash-commands";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
listWorkspaceFiles,
|
|
46
|
+
listWorkspaceFilesAndDirectories,
|
|
47
|
+
type WorkspaceFileLister,
|
|
48
|
+
} from "../workspace-file-search";
|
|
45
49
|
|
|
46
50
|
export type PromptSubmission = {
|
|
47
51
|
readonly draft: PromptDraft;
|
|
@@ -183,7 +187,10 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
183
187
|
() =>
|
|
184
188
|
fileQuery === undefined || fileCatalog.status !== "ready"
|
|
185
189
|
? []
|
|
186
|
-
: rankWorkspaceFiles(
|
|
190
|
+
: rankWorkspaceFiles(
|
|
191
|
+
listWorkspaceFilesAndDirectories(fileCatalog.files),
|
|
192
|
+
fileQuery,
|
|
193
|
+
),
|
|
187
194
|
[fileCatalog, fileQuery],
|
|
188
195
|
);
|
|
189
196
|
const suggestions =
|
|
@@ -223,13 +230,14 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
223
230
|
});
|
|
224
231
|
};
|
|
225
232
|
|
|
226
|
-
const selectFile = (
|
|
233
|
+
const selectFile = (match: FileMentionMatch) => {
|
|
234
|
+
const filePath = match.path;
|
|
227
235
|
const mention = findFileMention(state.draft.editor);
|
|
228
236
|
if (mention === undefined || locked) {
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
239
|
const importImage = props.importImage;
|
|
232
|
-
if (importImage === undefined) {
|
|
240
|
+
if (importImage === undefined || match.kind === "directory") {
|
|
233
241
|
insertFilePath(filePath);
|
|
234
242
|
return;
|
|
235
243
|
}
|
|
@@ -616,7 +624,7 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
616
624
|
const selectedCommand = suggestions[selectedIndex];
|
|
617
625
|
if (key.return) {
|
|
618
626
|
if (filePopupActive && selectedFile !== undefined) {
|
|
619
|
-
selectFile(selectedFile
|
|
627
|
+
selectFile(selectedFile);
|
|
620
628
|
} else if (selectedCommand !== undefined) {
|
|
621
629
|
submitDraft(createPromptDraft(`/${selectedCommand.name}`));
|
|
622
630
|
} else {
|
|
@@ -629,7 +637,7 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
629
637
|
if (selectedFile === undefined) {
|
|
630
638
|
setState((current) => ({ ...current, suggestionsDismissed: true }));
|
|
631
639
|
} else {
|
|
632
|
-
selectFile(selectedFile
|
|
640
|
+
selectFile(selectedFile);
|
|
633
641
|
}
|
|
634
642
|
} else if (selectedCommand !== undefined) {
|
|
635
643
|
setState(createPromptInputState(`/${selectedCommand.name} `));
|
|
@@ -26,16 +26,16 @@ export function TimelineRow(props: { item: TimelineItem }) {
|
|
|
26
26
|
if (item.label !== undefined) {
|
|
27
27
|
if (item.label === "assistant") {
|
|
28
28
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
29
|
+
<Box flexDirection="column" marginTop={1}>
|
|
30
|
+
<TimelineLabel label={item.label} />
|
|
31
31
|
<AssistantMarkdown text={item.text} />
|
|
32
|
-
</
|
|
32
|
+
</Box>
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<
|
|
38
|
-
<
|
|
37
|
+
<Box flexDirection="column" marginY={1}>
|
|
38
|
+
<TimelineLabel label={item.label} />
|
|
39
39
|
{item.userPrompt === undefined ? (
|
|
40
40
|
<Text color={colorForStatus(item.status)}>{formatTimelineItem(item)}</Text>
|
|
41
41
|
) : (
|
|
@@ -44,7 +44,7 @@ export function TimelineRow(props: { item: TimelineItem }) {
|
|
|
44
44
|
{renderItemBash(item)}
|
|
45
45
|
{renderItemDiff(item)}
|
|
46
46
|
{renderItemPlan(item)}
|
|
47
|
-
</
|
|
47
|
+
</Box>
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -60,10 +60,18 @@ export function TimelineRow(props: { item: TimelineItem }) {
|
|
|
60
60
|
|
|
61
61
|
export function AssistantStreamSectionRow(props: { item: AssistantStreamSectionItem }) {
|
|
62
62
|
return (
|
|
63
|
-
<
|
|
64
|
-
{props.item.showAssistantLabel ? <
|
|
63
|
+
<Box flexDirection="column" marginTop={props.item.showAssistantLabel ? 1 : 0}>
|
|
64
|
+
{props.item.showAssistantLabel ? <TimelineLabel label="assistant" /> : null}
|
|
65
65
|
<AssistantMarkdown text={props.item.markdown} />
|
|
66
|
-
</
|
|
66
|
+
</Box>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function TimelineLabel(props: { label: string }) {
|
|
71
|
+
return (
|
|
72
|
+
<Text color="cyan" bold>
|
|
73
|
+
- {props.label}
|
|
74
|
+
</Text>
|
|
67
75
|
);
|
|
68
76
|
}
|
|
69
77
|
|
package/src/tui/event-store.ts
CHANGED
|
@@ -205,6 +205,16 @@ export function reduceTuiProjection(
|
|
|
205
205
|
status: "running",
|
|
206
206
|
})),
|
|
207
207
|
);
|
|
208
|
+
case "model.retry.requested":
|
|
209
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
210
|
+
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
211
|
+
...item,
|
|
212
|
+
text: `model iteration ${event.iterationNumber} · waiting for retry selection`,
|
|
213
|
+
status: "running",
|
|
214
|
+
})),
|
|
215
|
+
);
|
|
216
|
+
case "model.retry.resolved":
|
|
217
|
+
return state;
|
|
208
218
|
case "model.request.finished":
|
|
209
219
|
return updateActiveTurn(state, event, policy, (turn) =>
|
|
210
220
|
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
@@ -825,7 +835,7 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
|
|
|
825
835
|
return `WebSearch ${toolQuery(input.args) ?? ""}`.trim();
|
|
826
836
|
}
|
|
827
837
|
if (input.name === "MemorySearch") {
|
|
828
|
-
return `MemorySearch ${memorySearchDetail(input.args)}`.trim();
|
|
838
|
+
return `MemorySearch ${toolPattern(input.args) ?? memorySearchDetail(input.args)}`.trim();
|
|
829
839
|
}
|
|
830
840
|
if (input.name === "MemoryCreate") {
|
|
831
841
|
return "MemoryCreate";
|
|
@@ -964,6 +974,8 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
964
974
|
if (!raw.ok) {
|
|
965
975
|
return base;
|
|
966
976
|
}
|
|
977
|
+
if ("format" in raw)
|
|
978
|
+
return `${base} -> ${raw.returnedResults} matching lines in ${raw.files.length} files`;
|
|
967
979
|
return `${base} -> ${raw.matches.length} derived memor${raw.matches.length === 1 ? "y" : "ies"}`;
|
|
968
980
|
case "memory_get":
|
|
969
981
|
if (!raw.ok) {
|