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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
2
|
+
import { TextInput } from "@inkjs/ui";
|
|
3
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { RemoteClient } from "../remote/client";
|
|
5
|
+
import type { RemoteSessionInfo } from "../remote/protocol";
|
|
6
|
+
|
|
7
|
+
/** An explicit network client mode; the normal App/controller path is untouched. */
|
|
8
|
+
export function RemoteApp({ client }: { client: RemoteClient }) {
|
|
9
|
+
const state = useSyncExternalStore(client.subscribe, client.getSnapshot);
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [workspaces, setWorkspaces] = useState<{ id: string; name: string }[]>([]);
|
|
12
|
+
const [sessions, setSessions] = useState<RemoteSessionInfo[]>([]);
|
|
13
|
+
const [workspace, setWorkspace] = useState<string | undefined>(client.workspaceId);
|
|
14
|
+
const [notice, setNotice] = useState(
|
|
15
|
+
"Select a workspace number. /workspaces returns here; /quit detaches.",
|
|
16
|
+
);
|
|
17
|
+
const [inputKey, setInputKey] = useState(0);
|
|
18
|
+
const [visibleCount, setVisibleCount] = useState(30);
|
|
19
|
+
const [browsing, setBrowsing] = useState(!client.sessionId);
|
|
20
|
+
const report = (error: unknown) =>
|
|
21
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
void client
|
|
24
|
+
.workspaces()
|
|
25
|
+
.then((result) => setWorkspaces(result.workspaces))
|
|
26
|
+
.catch(report);
|
|
27
|
+
}, [client]);
|
|
28
|
+
useInput((input, key) => {
|
|
29
|
+
if (key.ctrl && input === "c") exit();
|
|
30
|
+
});
|
|
31
|
+
const submit = async (text: string) => {
|
|
32
|
+
setInputKey((value) => value + 1);
|
|
33
|
+
if (!text.trim()) return;
|
|
34
|
+
if (text === "/quit") {
|
|
35
|
+
exit();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (text === "/workspaces") {
|
|
39
|
+
setWorkspace(undefined);
|
|
40
|
+
setBrowsing(true);
|
|
41
|
+
setSessions([]);
|
|
42
|
+
setWorkspaces((await client.workspaces()).workspaces);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (browsing) {
|
|
46
|
+
if (!workspace) {
|
|
47
|
+
const chosen = workspaces[Number(text) - 1];
|
|
48
|
+
if (!chosen) throw new Error("Enter a workspace number.");
|
|
49
|
+
setWorkspace(chosen.id);
|
|
50
|
+
setSessions((await client.sessions(chosen.id)).sessions);
|
|
51
|
+
setNotice(
|
|
52
|
+
"Enter a session number, or /new. A local session is explicitly attached when selected.",
|
|
53
|
+
);
|
|
54
|
+
} else if (text === "/new") {
|
|
55
|
+
await client.submit({ kind: "create", workspaceId: workspace });
|
|
56
|
+
setBrowsing(false);
|
|
57
|
+
} else {
|
|
58
|
+
const chosen = sessions[Number(text) - 1];
|
|
59
|
+
if (!chosen) throw new Error("Enter a session number or /new.");
|
|
60
|
+
if (chosen.owner === "local")
|
|
61
|
+
await client.submit({
|
|
62
|
+
kind: "adopt",
|
|
63
|
+
workspaceId: workspace,
|
|
64
|
+
sessionId: chosen.id,
|
|
65
|
+
});
|
|
66
|
+
else await client.select(chosen.id, workspace);
|
|
67
|
+
setBrowsing(false);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const sessionId = client.sessionId;
|
|
72
|
+
if (!sessionId) throw new Error("Waiting for the service to accept the session.");
|
|
73
|
+
const pending = state.view?.interaction;
|
|
74
|
+
if (text === "/stop") {
|
|
75
|
+
if (!state.view?.activeRequestId) throw new Error("No active task is shown.");
|
|
76
|
+
await client.submit({
|
|
77
|
+
kind: "stop",
|
|
78
|
+
sessionId,
|
|
79
|
+
targetRequestId: state.view.activeRequestId,
|
|
80
|
+
});
|
|
81
|
+
} else if (text === "/allow" || text === "/deny") {
|
|
82
|
+
if (pending?.kind !== "confirmation")
|
|
83
|
+
throw new Error("No confirmation is pending.");
|
|
84
|
+
await client.submit({
|
|
85
|
+
kind: "confirm",
|
|
86
|
+
sessionId,
|
|
87
|
+
interactionId: pending.id,
|
|
88
|
+
decision: text === "/allow" ? "allow" : "deny",
|
|
89
|
+
});
|
|
90
|
+
} else if (text.startsWith("/answer ") || text === "/dismiss") {
|
|
91
|
+
if (pending?.kind !== "question") throw new Error("No question is pending.");
|
|
92
|
+
await client.submit({
|
|
93
|
+
kind: "answer",
|
|
94
|
+
sessionId,
|
|
95
|
+
interactionId: pending.id,
|
|
96
|
+
selectedIndex: text === "/dismiss" ? null : Number(text.slice(8)) - 1,
|
|
97
|
+
});
|
|
98
|
+
} else if (text === "/history") {
|
|
99
|
+
await client.loadOlderHistory();
|
|
100
|
+
setVisibleCount((count) => count + 80);
|
|
101
|
+
setNotice(
|
|
102
|
+
"Older history loaded; use your terminal scrollback to read previous renders.",
|
|
103
|
+
);
|
|
104
|
+
} else if (text.startsWith("/")) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Commands: /stop /allow /deny /answer N /dismiss /history /workspaces /quit",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
await client.submit({ kind: "prompt", sessionId, prompt: text });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const view = state.view;
|
|
113
|
+
return (
|
|
114
|
+
<Box flexDirection="column">
|
|
115
|
+
<Text bold color="cyan">
|
|
116
|
+
Tinker · Service connection
|
|
117
|
+
</Text>
|
|
118
|
+
<Text>
|
|
119
|
+
{state.connection} · {view?.status ?? "select session"} · {state.pending}{" "}
|
|
120
|
+
unsubmitted
|
|
121
|
+
</Text>
|
|
122
|
+
<Text dimColor>{notice}</Text>
|
|
123
|
+
{state.error && <Text color="red">{safe(state.error)}</Text>}
|
|
124
|
+
{browsing ? (
|
|
125
|
+
<Box flexDirection="column">
|
|
126
|
+
{!workspace
|
|
127
|
+
? workspaces.map((w, i) => (
|
|
128
|
+
<Text key={w.id}>
|
|
129
|
+
{i + 1}. {safe(w.name)}
|
|
130
|
+
</Text>
|
|
131
|
+
))
|
|
132
|
+
: sessions.map((s, i) => (
|
|
133
|
+
<Text key={s.id}>
|
|
134
|
+
{i + 1}. {safe(s.title)} · {s.status} · {s.owner}
|
|
135
|
+
</Text>
|
|
136
|
+
))}
|
|
137
|
+
</Box>
|
|
138
|
+
) : (
|
|
139
|
+
<Box flexDirection="column">
|
|
140
|
+
<Text bold>{safe(view?.session.title ?? "Connecting…")}</Text>
|
|
141
|
+
{view?.history.messages.slice(-visibleCount).map((message) => (
|
|
142
|
+
<Box key={message.id} flexDirection="column" marginTop={1}>
|
|
143
|
+
<Text bold color={message.role === "user" ? "green" : "cyan"}>
|
|
144
|
+
{message.name ?? message.role}
|
|
145
|
+
</Text>
|
|
146
|
+
<Text>{safe(message.text)}</Text>
|
|
147
|
+
{message.toolCalls?.map((call) => (
|
|
148
|
+
<Text key={call.id} dimColor>
|
|
149
|
+
{call.name} {safe(call.arguments)}
|
|
150
|
+
</Text>
|
|
151
|
+
))}
|
|
152
|
+
</Box>
|
|
153
|
+
))}
|
|
154
|
+
{view?.streaming && (
|
|
155
|
+
<Box flexDirection="column">
|
|
156
|
+
<Text dimColor>Generating…</Text>
|
|
157
|
+
<Text>{safe(view.streaming.text)}</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
)}
|
|
160
|
+
{view?.tools
|
|
161
|
+
.filter((tool) => tool.status === "running")
|
|
162
|
+
.map((tool) => (
|
|
163
|
+
<Text key={tool.id} color="yellow">
|
|
164
|
+
{tool.name}: {safe(tool.arguments)}
|
|
165
|
+
</Text>
|
|
166
|
+
))}
|
|
167
|
+
{view?.interaction?.kind === "question" && (
|
|
168
|
+
<Box flexDirection="column">
|
|
169
|
+
<Text color="yellow">{safe(view.interaction.question)}</Text>
|
|
170
|
+
{view.interaction.options.map((option, i) => (
|
|
171
|
+
<Text key={i}>
|
|
172
|
+
{i + 1}. {safe(option.description)}
|
|
173
|
+
</Text>
|
|
174
|
+
))}
|
|
175
|
+
<Text>/answer N or /dismiss</Text>
|
|
176
|
+
</Box>
|
|
177
|
+
)}
|
|
178
|
+
{view?.interaction?.kind === "confirmation" && (
|
|
179
|
+
<Box flexDirection="column">
|
|
180
|
+
<Text color="yellow">{safe(view.interaction.command)}</Text>
|
|
181
|
+
<Text>{safe(view.interaction.reason)} · /allow or /deny</Text>
|
|
182
|
+
</Box>
|
|
183
|
+
)}
|
|
184
|
+
</Box>
|
|
185
|
+
)}
|
|
186
|
+
<Box marginTop={1}>
|
|
187
|
+
<Text color="cyan">› </Text>
|
|
188
|
+
<TextInput
|
|
189
|
+
key={inputKey}
|
|
190
|
+
placeholder={
|
|
191
|
+
browsing
|
|
192
|
+
? "Choose a workspace/session"
|
|
193
|
+
: "Send task; /stop cancels; /quit detaches"
|
|
194
|
+
}
|
|
195
|
+
onSubmit={(value) => {
|
|
196
|
+
void submit(value).catch(report);
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
</Box>
|
|
200
|
+
</Box>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
function safe(value: string): string {
|
|
204
|
+
return [...value]
|
|
205
|
+
.filter((character) => {
|
|
206
|
+
const code = character.codePointAt(0)!;
|
|
207
|
+
return code === 10 || code === 9 || (code >= 32 && (code < 127 || code > 159));
|
|
208
|
+
})
|
|
209
|
+
.join("");
|
|
210
|
+
}
|
|
@@ -205,12 +205,15 @@ export class TuiProjectionStore implements EventSink, AssistantTextDeltaSink {
|
|
|
205
205
|
return false;
|
|
206
206
|
}
|
|
207
207
|
this.assistantStreamAttempt = undefined;
|
|
208
|
-
if (
|
|
208
|
+
if (attempt.sectionCount === 0) {
|
|
209
209
|
return false;
|
|
210
210
|
}
|
|
211
211
|
this.appendCommitted({
|
|
212
212
|
id: `assistant-stream-retry-${attempt.iterationId}-${attempt.attemptNumber}`,
|
|
213
|
-
text:
|
|
213
|
+
text:
|
|
214
|
+
event.data.retryDisposition === "scheduled"
|
|
215
|
+
? "assistant response interrupted · retrying"
|
|
216
|
+
: "assistant response interrupted",
|
|
214
217
|
status: "info",
|
|
215
218
|
});
|
|
216
219
|
return true;
|
|
@@ -54,6 +54,9 @@ export type TuiSessionBinding = {
|
|
|
54
54
|
subscribeBashGuard(listener: () => void): () => void;
|
|
55
55
|
setYoloMode(enabled: boolean): void;
|
|
56
56
|
resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
|
|
57
|
+
providerRetry?: RuntimeSession["providerRetry"];
|
|
58
|
+
subscribeProviderRetry?: RuntimeSession["subscribeProviderRetry"];
|
|
59
|
+
resolveProviderRetry?: RuntimeSession["resolveProviderRetry"];
|
|
57
60
|
askUser(): AskUserSnapshot;
|
|
58
61
|
subscribeAskUser(listener: () => void): () => void;
|
|
59
62
|
resolveAskUser(response: AskUserResolution): Promise<void>;
|
|
@@ -267,6 +270,11 @@ export function managedTuiBinding(input: {
|
|
|
267
270
|
setYoloMode: (enabled) => input.runtimeSession.setYoloMode(enabled),
|
|
268
271
|
resolveBashConfirmation: (decision) =>
|
|
269
272
|
input.runtimeSession.resolveBashConfirmation(decision),
|
|
273
|
+
providerRetry: () => input.runtimeSession.providerRetry(),
|
|
274
|
+
subscribeProviderRetry: (listener) =>
|
|
275
|
+
input.runtimeSession.subscribeProviderRetry(listener),
|
|
276
|
+
resolveProviderRetry: (requestId, decision) =>
|
|
277
|
+
input.runtimeSession.resolveProviderRetry(requestId, decision),
|
|
270
278
|
askUser: () => input.runtimeSession.askUser(),
|
|
271
279
|
subscribeAskUser: (listener) => input.runtimeSession.subscribeAskUser(listener),
|
|
272
280
|
resolveAskUser: (response) => input.runtimeSession.resolveAskUser(response),
|
|
@@ -93,6 +93,27 @@ export function createWorkspaceFileLister(
|
|
|
93
93
|
|
|
94
94
|
export const listWorkspaceFiles = createWorkspaceFileLister();
|
|
95
95
|
|
|
96
|
+
export function deriveWorkspaceDirectories(files: readonly string[]): string[] {
|
|
97
|
+
const directories = new Set<string>();
|
|
98
|
+
|
|
99
|
+
for (const filePath of files) {
|
|
100
|
+
for (let index = 0; index < filePath.length; index += 1) {
|
|
101
|
+
const char = filePath[index];
|
|
102
|
+
if ((char === "/" || char === "\\") && index > 0) {
|
|
103
|
+
directories.add(filePath.slice(0, index + 1));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...directories];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listWorkspaceFilesAndDirectories(
|
|
112
|
+
files: readonly string[],
|
|
113
|
+
): readonly string[] {
|
|
114
|
+
return [...files, ...deriveWorkspaceDirectories(files)];
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
function splitPaths(stdout: string): string[] {
|
|
97
118
|
return stdout
|
|
98
119
|
.split("\n")
|