tinker-agent 2.9.0 → 2.11.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 +69 -1
- package/README.md +30 -1
- package/package.json +5 -3
- package/src/agent/loop.ts +50 -13
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-provider-retry.ts +115 -0
- package/src/agent/runtime-session-contracts.ts +11 -0
- package/src/agent/runtime-session.ts +29 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/cli/tui-runner.tsx +1 -0
- package/src/context/context-swap-renderer.ts +14 -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/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 +87 -37
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/scoped-query-database.ts +27 -0
- package/src/session/session-history-access.ts +4 -3
- package/src/session/session-store.ts +7 -3
- package/src/tools/bash-task.ts +26 -16
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +148 -155
- package/src/tools/read.ts +0 -9
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +30 -2
- package/src/tui/app.tsx +45 -3
- 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 +25 -2
- package/src/tui/file-mention.ts +29 -5
- package/src/tui/remote-app.tsx +210 -0
- 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
|
@@ -14,6 +14,7 @@ export type ShellProcessHandle = {
|
|
|
14
14
|
readonly mode: ShellProcessMode;
|
|
15
15
|
readonly exitCode: number | null;
|
|
16
16
|
readonly signalCode: NodeJS.Signals | null;
|
|
17
|
+
readonly outputClosed: boolean;
|
|
17
18
|
wait(): Promise<ProcessExitResult>;
|
|
18
19
|
waitForOutputClose(): Promise<void>;
|
|
19
20
|
write?(chars: string): Promise<number>;
|
|
@@ -35,6 +36,8 @@ export async function spawnShellProcess(input: {
|
|
|
35
36
|
command: string;
|
|
36
37
|
cwd: string;
|
|
37
38
|
cwdFilePath: string;
|
|
39
|
+
cols?: number;
|
|
40
|
+
rows?: number;
|
|
38
41
|
onOutput(bytes: Uint8Array): void;
|
|
39
42
|
}): Promise<ShellProcessHandle> {
|
|
40
43
|
const env = {
|
|
@@ -65,7 +68,16 @@ async function spawnPipeShellProcess(input: {
|
|
|
65
68
|
pipeOutput(child.stderr, (bytes) => input.onOutput(bytes));
|
|
66
69
|
|
|
67
70
|
const exit = waitForNodeProcessExit(child);
|
|
68
|
-
|
|
71
|
+
let outputClosed = false;
|
|
72
|
+
let resolveOutputClose: () => void;
|
|
73
|
+
const close = new Promise<void>((resolve) => {
|
|
74
|
+
resolveOutputClose = resolve;
|
|
75
|
+
});
|
|
76
|
+
const settleOutputClose = () => {
|
|
77
|
+
outputClosed = true;
|
|
78
|
+
resolveOutputClose();
|
|
79
|
+
};
|
|
80
|
+
void waitForNodeProcessClose(child).then(settleOutputClose);
|
|
69
81
|
if (child.pid === undefined) {
|
|
70
82
|
const result = await exit;
|
|
71
83
|
throw new Error(result.error ?? "Bash process failed to start.");
|
|
@@ -80,15 +92,25 @@ async function spawnPipeShellProcess(input: {
|
|
|
80
92
|
get signalCode() {
|
|
81
93
|
return child.signalCode;
|
|
82
94
|
},
|
|
95
|
+
get outputClosed() {
|
|
96
|
+
return outputClosed;
|
|
97
|
+
},
|
|
83
98
|
wait: () => exit,
|
|
84
99
|
waitForOutputClose: () => close,
|
|
85
|
-
close() {
|
|
100
|
+
close() {
|
|
101
|
+
child.stdin.destroy();
|
|
102
|
+
child.stdout.destroy();
|
|
103
|
+
child.stderr.destroy();
|
|
104
|
+
settleOutputClose();
|
|
105
|
+
},
|
|
86
106
|
};
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
function spawnPtyShellProcess(input: {
|
|
90
110
|
cwd: string;
|
|
91
111
|
env: NodeJS.ProcessEnv;
|
|
112
|
+
cols?: number;
|
|
113
|
+
rows?: number;
|
|
92
114
|
onOutput(bytes: Uint8Array): void;
|
|
93
115
|
}): ShellProcessHandle {
|
|
94
116
|
let terminalEnded = false;
|
|
@@ -125,8 +147,8 @@ function spawnPtyShellProcess(input: {
|
|
|
125
147
|
GIT_PAGER: "cat",
|
|
126
148
|
},
|
|
127
149
|
terminal: {
|
|
128
|
-
cols: TERMINAL_SCREEN_COLUMNS,
|
|
129
|
-
rows: TERMINAL_SCREEN_ROWS,
|
|
150
|
+
cols: input.cols ?? TERMINAL_SCREEN_COLUMNS,
|
|
151
|
+
rows: input.rows ?? TERMINAL_SCREEN_ROWS,
|
|
130
152
|
name: "xterm-256color",
|
|
131
153
|
data(_terminal, bytes) {
|
|
132
154
|
input.onOutput(new Uint8Array(bytes));
|
|
@@ -205,6 +227,9 @@ function spawnPtyShellProcess(input: {
|
|
|
205
227
|
get signalCode() {
|
|
206
228
|
return subprocess.signalCode;
|
|
207
229
|
},
|
|
230
|
+
get outputClosed() {
|
|
231
|
+
return terminalEnded;
|
|
232
|
+
},
|
|
208
233
|
wait: () => exit,
|
|
209
234
|
waitForOutputClose: () => terminalExit,
|
|
210
235
|
write,
|
|
@@ -212,6 +237,7 @@ function spawnPtyShellProcess(input: {
|
|
|
212
237
|
if (!terminal.closed) {
|
|
213
238
|
terminal.close();
|
|
214
239
|
}
|
|
240
|
+
settleTerminalExit();
|
|
215
241
|
},
|
|
216
242
|
};
|
|
217
243
|
}
|
package/src/tools/task-stop.ts
CHANGED
|
@@ -37,12 +37,13 @@ export function createTaskStopToolExecutor(options: {
|
|
|
37
37
|
try {
|
|
38
38
|
const result = await options.taskManager.stopTask(parsed.taskId, "tool");
|
|
39
39
|
return {
|
|
40
|
-
ok:
|
|
40
|
+
ok: result.task.error === undefined,
|
|
41
41
|
taskId: parsed.taskId,
|
|
42
42
|
task: result.task,
|
|
43
43
|
status: result.task.status,
|
|
44
44
|
requestedSignal: result.requestedSignal,
|
|
45
45
|
escalated: result.escalated,
|
|
46
|
+
...(result.task.error === undefined ? {} : { error: result.task.error }),
|
|
46
47
|
};
|
|
47
48
|
} catch (error) {
|
|
48
49
|
const inspection = options.taskManager.inspectTask(parsed.taskId);
|
|
@@ -3,16 +3,25 @@ import { Terminal } from "@xterm/headless";
|
|
|
3
3
|
|
|
4
4
|
export const TERMINAL_SCREEN_ROWS = 24;
|
|
5
5
|
export const TERMINAL_SCREEN_COLUMNS = 80;
|
|
6
|
+
export const MIN_TERMINAL_COLUMNS = 2;
|
|
7
|
+
export const MAX_TERMINAL_DIMENSION = 1_000;
|
|
6
8
|
|
|
7
9
|
export type TerminalScreen = {
|
|
10
|
+
readonly rows: number;
|
|
11
|
+
readonly columns: number;
|
|
8
12
|
write(bytes: Uint8Array): Promise<void>;
|
|
9
13
|
flush(): Promise<void>;
|
|
10
14
|
text(): string;
|
|
11
15
|
dispose(): void;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
|
-
export function createTerminalScreen(
|
|
15
|
-
|
|
18
|
+
export function createTerminalScreen(
|
|
19
|
+
options: { cols?: number; rows?: number } = {},
|
|
20
|
+
): TerminalScreen {
|
|
21
|
+
return new HeadlessTerminalScreen(
|
|
22
|
+
options.rows ?? TERMINAL_SCREEN_ROWS,
|
|
23
|
+
options.cols ?? TERMINAL_SCREEN_COLUMNS,
|
|
24
|
+
);
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export class HeadlessTerminalScreen implements TerminalScreen {
|
package/src/tools/types.ts
CHANGED
|
@@ -92,16 +92,27 @@ export type DeleteFileRawResult = {
|
|
|
92
92
|
|
|
93
93
|
export type GlobRawResult = {
|
|
94
94
|
ok: boolean;
|
|
95
|
-
pattern
|
|
95
|
+
pattern?: string;
|
|
96
96
|
searchPath: string;
|
|
97
97
|
absoluteSearchPath?: string;
|
|
98
98
|
matches?: string[];
|
|
99
|
+
/** Number of paths returned, including in legacy unpaginated results. */
|
|
99
100
|
matchCount?: number;
|
|
101
|
+
/** Exact total before pagination; absent in legacy results. */
|
|
102
|
+
totalMatches?: number;
|
|
103
|
+
returnedCount?: number;
|
|
104
|
+
appliedOffset?: number;
|
|
105
|
+
hasMore?: boolean;
|
|
106
|
+
nextOffset?: number;
|
|
100
107
|
ignored?: string[];
|
|
101
108
|
error?: string;
|
|
102
109
|
};
|
|
103
110
|
|
|
104
|
-
export type GrepOutputMode =
|
|
111
|
+
export type GrepOutputMode =
|
|
112
|
+
| "content"
|
|
113
|
+
| "files_with_matches"
|
|
114
|
+
| "count"
|
|
115
|
+
| "count-matches";
|
|
105
116
|
|
|
106
117
|
export type GrepRawResult = {
|
|
107
118
|
ok: boolean;
|
|
@@ -113,10 +124,27 @@ export type GrepRawResult = {
|
|
|
113
124
|
numFiles: number;
|
|
114
125
|
content?: string;
|
|
115
126
|
numLines?: number;
|
|
127
|
+
/** Structured count records; paths are unescaped and never parsed from display text. */
|
|
128
|
+
counts?: { filePath: string; count: number }[];
|
|
129
|
+
/** Sum on this page: matching lines for count, individual matches for count-matches. */
|
|
116
130
|
numMatches?: number;
|
|
131
|
+
/** Collected pagination units; legacy content results counted all output lines. Not a global total if searchIncomplete. */
|
|
132
|
+
totalResults?: number;
|
|
133
|
+
/** Selected pagination units, excluding context and nearby matches shown as context. */
|
|
134
|
+
returnedResults?: number;
|
|
135
|
+
paginationUnit?: "matching_lines" | "match_events" | "files";
|
|
136
|
+
/** More collected pagination units are available, independently of search completeness. */
|
|
137
|
+
hasMore?: boolean;
|
|
138
|
+
nextOffset?: number;
|
|
117
139
|
appliedLimit?: number;
|
|
118
140
|
appliedOffset?: number;
|
|
141
|
+
/** True for interrupted searches (e.g. timeout or output buffer limit), not pagination. */
|
|
142
|
+
searchIncomplete?: boolean;
|
|
143
|
+
/** The search stopped early, so requested context cannot be guaranteed complete. */
|
|
144
|
+
contextMayBeIncomplete?: boolean;
|
|
145
|
+
/** Legacy default exclusions, not observed skips. New Grep results omit this field. */
|
|
119
146
|
ignored?: string[];
|
|
147
|
+
/** Compatibility flag combining pagination and interrupted search; prefer explicit fields above. */
|
|
120
148
|
truncated?: boolean;
|
|
121
149
|
error?: string;
|
|
122
150
|
};
|
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,
|
|
@@ -143,6 +144,12 @@ export function App(props: AppProps) {
|
|
|
143
144
|
() => binding.bashGuard(),
|
|
144
145
|
() => binding.bashGuard(),
|
|
145
146
|
);
|
|
147
|
+
const providerRetry = useSyncExternalStore(
|
|
148
|
+
(listener) => binding.subscribeProviderRetry?.(listener) ?? (() => undefined),
|
|
149
|
+
() => binding.providerRetry?.() ?? EMPTY_PROVIDER_RETRY,
|
|
150
|
+
() => EMPTY_PROVIDER_RETRY,
|
|
151
|
+
);
|
|
152
|
+
const pendingProviderRetry = providerRetry.pending;
|
|
146
153
|
const askUser = useSyncExternalStore(
|
|
147
154
|
(listener) => binding.subscribeAskUser(listener),
|
|
148
155
|
() => binding.askUser(),
|
|
@@ -282,7 +289,12 @@ export function App(props: AppProps) {
|
|
|
282
289
|
setIsCancelling(true);
|
|
283
290
|
setNotice("Cancelling current turn...");
|
|
284
291
|
},
|
|
285
|
-
{
|
|
292
|
+
{
|
|
293
|
+
isActive:
|
|
294
|
+
executionRunning &&
|
|
295
|
+
askUser.pending === undefined &&
|
|
296
|
+
pendingProviderRetry === undefined,
|
|
297
|
+
},
|
|
286
298
|
);
|
|
287
299
|
|
|
288
300
|
const closeResumePicker = () => {
|
|
@@ -912,7 +924,8 @@ export function App(props: AppProps) {
|
|
|
912
924
|
status={
|
|
913
925
|
isCancelling
|
|
914
926
|
? "cancelling"
|
|
915
|
-
: askUser.pending !== undefined
|
|
927
|
+
: askUser.pending !== undefined ||
|
|
928
|
+
pendingProviderRetry !== undefined
|
|
916
929
|
? "waiting_for_answer"
|
|
917
930
|
: executionRunning
|
|
918
931
|
? "running"
|
|
@@ -924,7 +937,35 @@ export function App(props: AppProps) {
|
|
|
924
937
|
/>
|
|
925
938
|
</Box>
|
|
926
939
|
<Box marginTop={1} flexDirection="column" flexShrink={0}>
|
|
927
|
-
{
|
|
940
|
+
{pendingProviderRetry !== undefined ? (
|
|
941
|
+
<AskUser
|
|
942
|
+
key={pendingProviderRetry.requestId}
|
|
943
|
+
title="Provider request failed"
|
|
944
|
+
question={`Automatic retries exhausted. ${pendingProviderRetry.failure.error.slice(0, 500)}`}
|
|
945
|
+
options={[
|
|
946
|
+
{ description: "Retry again" },
|
|
947
|
+
{ description: "End this turn" },
|
|
948
|
+
]}
|
|
949
|
+
dismissLabel="end this turn"
|
|
950
|
+
onSelect={(index) => {
|
|
951
|
+
void binding
|
|
952
|
+
.resolveProviderRetry?.(
|
|
953
|
+
pendingProviderRetry.requestId,
|
|
954
|
+
index === 0 ? "retry" : "stop",
|
|
955
|
+
)
|
|
956
|
+
.catch((error: unknown) =>
|
|
957
|
+
setNotice(`Retry selection failed: ${errorMessage(error)}`),
|
|
958
|
+
);
|
|
959
|
+
}}
|
|
960
|
+
onDismiss={() => {
|
|
961
|
+
void binding
|
|
962
|
+
.resolveProviderRetry?.(pendingProviderRetry.requestId, "stop")
|
|
963
|
+
.catch((error: unknown) =>
|
|
964
|
+
setNotice(`Retry selection failed: ${errorMessage(error)}`),
|
|
965
|
+
);
|
|
966
|
+
}}
|
|
967
|
+
/>
|
|
968
|
+
) : askUser.pending !== undefined ? (
|
|
928
969
|
<AskUser
|
|
929
970
|
question={askUser.pending.question}
|
|
930
971
|
options={askUser.pending.options}
|
|
@@ -977,6 +1018,7 @@ export function App(props: AppProps) {
|
|
|
977
1018
|
isCopying ||
|
|
978
1019
|
isCancelling ||
|
|
979
1020
|
askUser.pending !== undefined ||
|
|
1021
|
+
pendingProviderRetry !== undefined ||
|
|
980
1022
|
bashGuard.pending !== undefined
|
|
981
1023
|
}
|
|
982
1024
|
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) => ({
|
|
@@ -873,6 +883,9 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
873
883
|
? base
|
|
874
884
|
: `${base} -> ${raw.asset.mimeType}, ${raw.asset.width}x${raw.asset.height}, ${raw.asset.byteLength} bytes`;
|
|
875
885
|
case "glob":
|
|
886
|
+
if (raw.ok && raw.totalMatches !== undefined) {
|
|
887
|
+
return `${base} -> ${raw.returnedCount ?? raw.matches?.length ?? 0} of ${raw.totalMatches} matches`;
|
|
888
|
+
}
|
|
876
889
|
return raw.ok && raw.matchCount !== undefined
|
|
877
890
|
? `${base} -> ${raw.matchCount} match${raw.matchCount === 1 ? "" : "es"}`
|
|
878
891
|
: base;
|
|
@@ -881,11 +894,21 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
881
894
|
return `${base} -> ${raw.numLines} line${raw.numLines === 1 ? "" : "s"}`;
|
|
882
895
|
}
|
|
883
896
|
if (
|
|
884
|
-
raw.mode === "count" &&
|
|
897
|
+
(raw.mode === "count" || raw.mode === "count-matches") &&
|
|
885
898
|
raw.numMatches !== undefined &&
|
|
886
899
|
raw.numFiles !== undefined
|
|
887
900
|
) {
|
|
888
|
-
|
|
901
|
+
const unit =
|
|
902
|
+
raw.mode === "count"
|
|
903
|
+
? `matching line${raw.numMatches === 1 ? "" : "s"}`
|
|
904
|
+
: `match${raw.numMatches === 1 ? "" : "es"}`;
|
|
905
|
+
const scope =
|
|
906
|
+
raw.appliedLimit !== undefined || (raw.appliedOffset ?? 0) > 0
|
|
907
|
+
? " (this page)"
|
|
908
|
+
: raw.truncated
|
|
909
|
+
? " (partial results)"
|
|
910
|
+
: "";
|
|
911
|
+
return `${base} -> ${raw.numMatches} ${unit} across ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}${scope}`;
|
|
889
912
|
}
|
|
890
913
|
return `${base} -> ${raw.numFiles} file${raw.numFiles === 1 ? "" : "s"}`;
|
|
891
914
|
case "read":
|
package/src/tui/file-mention.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type FileMentionMatch = {
|
|
|
12
12
|
path: string;
|
|
13
13
|
indices: readonly number[];
|
|
14
14
|
score: number;
|
|
15
|
+
kind: "file" | "directory";
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export function findFileMention(editor: LineEditorState): FileMention | undefined {
|
|
@@ -76,8 +77,13 @@ export function rankWorkspaceFiles(
|
|
|
76
77
|
): FileMentionMatch[] {
|
|
77
78
|
if (query === "") {
|
|
78
79
|
return files
|
|
79
|
-
.map((filePath) => ({
|
|
80
|
-
|
|
80
|
+
.map((filePath) => ({
|
|
81
|
+
path: filePath,
|
|
82
|
+
indices: [],
|
|
83
|
+
score: 0,
|
|
84
|
+
kind: fileMentionKind(filePath),
|
|
85
|
+
}))
|
|
86
|
+
.sort((left, right) => compareShallowPaths(left, right))
|
|
81
87
|
.slice(0, limit);
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -104,6 +110,7 @@ function fuzzyMatchPath(filePath: string, query: string): FileMentionMatch | und
|
|
|
104
110
|
path: filePath,
|
|
105
111
|
indices: basenameMatch.indices,
|
|
106
112
|
score: basenameMatch.score + 200,
|
|
113
|
+
kind: fileMentionKind(filePath),
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -115,6 +122,7 @@ function fuzzyMatchPath(filePath: string, query: string): FileMentionMatch | und
|
|
|
115
122
|
path: filePath,
|
|
116
123
|
indices: fullPathMatch.indices,
|
|
117
124
|
score: fullPathMatch.score,
|
|
125
|
+
kind: fileMentionKind(filePath),
|
|
118
126
|
};
|
|
119
127
|
}
|
|
120
128
|
|
|
@@ -167,6 +175,11 @@ function compareFileMatches(left: FileMentionMatch, right: FileMentionMatch): nu
|
|
|
167
175
|
return right.score - left.score;
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
const kindDifference = left.kind === right.kind ? 0 : left.kind === "file" ? -1 : 1;
|
|
179
|
+
if (kindDifference !== 0) {
|
|
180
|
+
return kindDifference;
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
const depthDifference = pathDepth(left.path) - pathDepth(right.path);
|
|
171
184
|
if (depthDifference !== 0) {
|
|
172
185
|
return depthDifference;
|
|
@@ -178,9 +191,16 @@ function compareFileMatches(left: FileMentionMatch, right: FileMentionMatch): nu
|
|
|
178
191
|
: lengthDifference;
|
|
179
192
|
}
|
|
180
193
|
|
|
181
|
-
function compareShallowPaths(left:
|
|
182
|
-
const
|
|
183
|
-
|
|
194
|
+
function compareShallowPaths(left: FileMentionMatch, right: FileMentionMatch): number {
|
|
195
|
+
const kindDifference = left.kind === right.kind ? 0 : left.kind === "file" ? -1 : 1;
|
|
196
|
+
if (kindDifference !== 0) {
|
|
197
|
+
return kindDifference;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const depthDifference = pathDepth(left.path) - pathDepth(right.path);
|
|
201
|
+
return depthDifference === 0
|
|
202
|
+
? comparePathText(left.path, right.path)
|
|
203
|
+
: depthDifference;
|
|
184
204
|
}
|
|
185
205
|
|
|
186
206
|
function comparePathText(left: string, right: string): number {
|
|
@@ -196,6 +216,10 @@ function comparePathText(left: string, right: string): number {
|
|
|
196
216
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
197
217
|
}
|
|
198
218
|
|
|
219
|
+
function fileMentionKind(filePath: string): FileMentionMatch["kind"] {
|
|
220
|
+
return filePath.endsWith("/") || filePath.endsWith("\\") ? "directory" : "file";
|
|
221
|
+
}
|
|
222
|
+
|
|
199
223
|
function pathDepth(filePath: string): number {
|
|
200
224
|
return [...filePath].filter((char) => char === "/" || char === "\\").length;
|
|
201
225
|
}
|