@fastino-ai/pioneer-cli 0.2.8 → 0.2.10
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/README.md +32 -12
- package/package.json +2 -1
- package/src/api.ts +108 -7
- package/src/chat/ChatApp.tsx +5 -2
- package/src/config.ts +15 -0
- package/src/index.tsx +2523 -876
package/src/index.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useState, useEffect, useRef } from "react";
|
|
8
|
-
import { render, Box, Text, useApp, useInput, useStdin, Static } from "ink";
|
|
8
|
+
import { render, Box, Text, useApp, useInput, useStdin, useStdout, Static } from "ink";
|
|
9
9
|
import Spinner from "ink-spinner";
|
|
10
10
|
import TextInput from "ink-text-input";
|
|
11
11
|
import * as fs from "fs";
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
getApiKey,
|
|
16
16
|
getBaseUrl,
|
|
17
17
|
getMleModel,
|
|
18
|
+
getLastAgentConversationId,
|
|
19
|
+
setLastAgentConversationId,
|
|
18
20
|
saveConfig,
|
|
19
21
|
clearApiKey,
|
|
20
22
|
getHfToken,
|
|
@@ -165,6 +167,8 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
165
167
|
"format-results",
|
|
166
168
|
"reasoning-trace",
|
|
167
169
|
"use-meta-felix",
|
|
170
|
+
"json",
|
|
171
|
+
"all",
|
|
168
172
|
]);
|
|
169
173
|
|
|
170
174
|
function parseArgs(argv: string[]): {
|
|
@@ -274,24 +278,28 @@ interface TelemetryPromptProps {
|
|
|
274
278
|
|
|
275
279
|
const TelemetryPrompt: React.FC<TelemetryPromptProps> = ({ onComplete }) => {
|
|
276
280
|
const [selected, setSelected] = useState<"yes" | "no">("yes");
|
|
281
|
+
const { isRawModeSupported } = useStdin();
|
|
277
282
|
|
|
278
|
-
useInput(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
283
|
+
useInput(
|
|
284
|
+
(input, key) => {
|
|
285
|
+
if (key.leftArrow || key.rightArrow) {
|
|
286
|
+
setSelected((s) => (s === "yes" ? "no" : "yes"));
|
|
287
|
+
}
|
|
288
|
+
if (key.return) {
|
|
289
|
+
setTelemetryEnabled(selected === "yes");
|
|
290
|
+
onComplete();
|
|
291
|
+
}
|
|
292
|
+
if (input === "y" || input === "Y") {
|
|
293
|
+
setTelemetryEnabled(true);
|
|
294
|
+
onComplete();
|
|
295
|
+
}
|
|
296
|
+
if (input === "n" || input === "N") {
|
|
297
|
+
setTelemetryEnabled(false);
|
|
298
|
+
onComplete();
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{ isActive: isRawModeSupported }
|
|
302
|
+
);
|
|
295
303
|
|
|
296
304
|
return (
|
|
297
305
|
<Box flexDirection="column" paddingX={1}>
|
|
@@ -725,8 +733,12 @@ function toHistoryMessages(
|
|
|
725
733
|
return items
|
|
726
734
|
.filter((item) => Boolean(item && typeof item.content === "string" && typeof item.role === "string"))
|
|
727
735
|
.map((item) => {
|
|
736
|
+
const role =
|
|
737
|
+
item.role === "user" || item.role === "assistant" || item.role === "tool"
|
|
738
|
+
? item.role
|
|
739
|
+
: "assistant";
|
|
728
740
|
const message: HistoryMessage = {
|
|
729
|
-
role
|
|
741
|
+
role,
|
|
730
742
|
content: item.content,
|
|
731
743
|
};
|
|
732
744
|
|
|
@@ -735,7 +747,20 @@ function toHistoryMessages(
|
|
|
735
747
|
message.tool_call_id = sessionMessage.tool_call_id;
|
|
736
748
|
}
|
|
737
749
|
if (sessionMessage.tool_calls && Array.isArray(sessionMessage.tool_calls)) {
|
|
738
|
-
message.tool_calls = sessionMessage.tool_calls
|
|
750
|
+
message.tool_calls = sessionMessage.tool_calls
|
|
751
|
+
.filter(
|
|
752
|
+
(call): call is { id: string; name: string; args: Record<string, unknown> } =>
|
|
753
|
+
Boolean(call) &&
|
|
754
|
+
typeof call.id === "string" &&
|
|
755
|
+
typeof call.name === "string" &&
|
|
756
|
+
typeof call.args === "object" &&
|
|
757
|
+
call.args !== null
|
|
758
|
+
)
|
|
759
|
+
.map((call) => ({
|
|
760
|
+
id: call.id,
|
|
761
|
+
name: call.name,
|
|
762
|
+
args: call.args,
|
|
763
|
+
}));
|
|
739
764
|
}
|
|
740
765
|
|
|
741
766
|
return message;
|
|
@@ -757,6 +782,11 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
|
|
|
757
782
|
const finish = (code: number) => {
|
|
758
783
|
setTimeout(() => process.exit(code), 300);
|
|
759
784
|
};
|
|
785
|
+
const persistConversation = (nextConversationId?: string) => {
|
|
786
|
+
if (nextConversationId) {
|
|
787
|
+
setLastAgentConversationId(nextConversationId);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
760
790
|
const getLatestAssistantContent = (
|
|
761
791
|
messages: HistoryMessage[] | undefined
|
|
762
792
|
): string => {
|
|
@@ -838,6 +868,9 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
|
|
|
838
868
|
throw new Error(lastError || result.error || "Agent fallback request failed.");
|
|
839
869
|
}
|
|
840
870
|
setStream(result.data?.answer ?? "");
|
|
871
|
+
if (result.data?.conversation_id) {
|
|
872
|
+
persistConversation(result.data.conversation_id);
|
|
873
|
+
}
|
|
841
874
|
if (!result.data?.answer) {
|
|
842
875
|
setError("Agent returned no response content.");
|
|
843
876
|
setState("error");
|
|
@@ -982,6 +1015,7 @@ function AgentInteractiveCommand({ message, conversationId, history }: AgentInte
|
|
|
982
1015
|
}
|
|
983
1016
|
doneHistory.current = messages;
|
|
984
1017
|
wsSessionId = sessionId;
|
|
1018
|
+
persistConversation(sessionId);
|
|
985
1019
|
},
|
|
986
1020
|
}, {
|
|
987
1021
|
history: mergedHistory,
|
|
@@ -1089,59 +1123,244 @@ function formatAutoAgentTurn(turn: AutoAgentTurn): string {
|
|
|
1089
1123
|
return `${prefix}${turn.content}`;
|
|
1090
1124
|
}
|
|
1091
1125
|
|
|
1092
|
-
function AutoAgentInteractiveSession({
|
|
1126
|
+
export function AutoAgentInteractiveSession({
|
|
1093
1127
|
conversationId: initialConversationId,
|
|
1094
1128
|
history,
|
|
1095
1129
|
mode,
|
|
1096
1130
|
firstMessage,
|
|
1131
|
+
allowSessionCreation = true,
|
|
1097
1132
|
}: {
|
|
1098
1133
|
conversationId?: string;
|
|
1099
1134
|
history?: api.AgentChatHistoryItem[];
|
|
1100
1135
|
mode: "standard" | "research";
|
|
1101
1136
|
firstMessage?: string;
|
|
1137
|
+
allowSessionCreation?: boolean;
|
|
1102
1138
|
}) {
|
|
1139
|
+
const { isRawModeSupported } = useStdin();
|
|
1103
1140
|
const [input, setInput] = useState("");
|
|
1104
1141
|
const [isLoading, setIsLoading] = useState(false);
|
|
1105
1142
|
const [error, setError] = useState("");
|
|
1106
1143
|
const [statusHint, setStatusHint] = useState("Start typing...");
|
|
1107
1144
|
const [conversationId, setConversationId] = useState(initialConversationId);
|
|
1108
1145
|
const [historyState, setHistoryState] = useState<api.AgentChatHistoryItem[]>(history ?? []);
|
|
1146
|
+
const normalizeTurnRole = (role: string): AutoAgentTurnRole => {
|
|
1147
|
+
const normalized = role?.trim().toLowerCase();
|
|
1148
|
+
return normalized === "user" || normalized === "human" ? "user" : "assistant";
|
|
1149
|
+
};
|
|
1150
|
+
const [isHistoryLoading, setIsHistoryLoading] = useState(
|
|
1151
|
+
Boolean(initialConversationId) && history === undefined
|
|
1152
|
+
);
|
|
1153
|
+
const [isResumeSessionUsable, setIsResumeSessionUsable] = useState(true);
|
|
1109
1154
|
const [turns, setTurns] = useState<AutoAgentTurn[]>(
|
|
1110
|
-
(history ?? []).map((entry) =>
|
|
1111
|
-
entry.role
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
)
|
|
1155
|
+
(history ?? []).map((entry) => ({
|
|
1156
|
+
role: normalizeTurnRole(entry.role),
|
|
1157
|
+
content: entry.content,
|
|
1158
|
+
}))
|
|
1115
1159
|
);
|
|
1160
|
+
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
|
1161
|
+
const [inputHistoryIndex, setInputHistoryIndex] = useState(-1);
|
|
1116
1162
|
const didSeedFirstMessage = useRef(false);
|
|
1163
|
+
const setInputValue = (nextValue: string) => {
|
|
1164
|
+
setInput(nextValue);
|
|
1165
|
+
setInputHistoryIndex(-1);
|
|
1166
|
+
};
|
|
1167
|
+
const normalizeSessionUserHistory = (items: api.AgentChatHistoryItem[]) =>
|
|
1168
|
+
items
|
|
1169
|
+
.filter((message) => {
|
|
1170
|
+
const normalizedRole = message.role?.trim().toLowerCase();
|
|
1171
|
+
return (
|
|
1172
|
+
(normalizedRole === "user" || normalizedRole === "human") &&
|
|
1173
|
+
typeof message.content === "string"
|
|
1174
|
+
);
|
|
1175
|
+
})
|
|
1176
|
+
.map((message) => message.content.trim())
|
|
1177
|
+
.filter((content): content is string => content.length > 0);
|
|
1178
|
+
const mapHistoryToTurns = (items: api.AgentChatHistoryItem[]) =>
|
|
1179
|
+
items.map((entry) => ({
|
|
1180
|
+
role: normalizeTurnRole(entry.role),
|
|
1181
|
+
content: entry.content,
|
|
1182
|
+
}));
|
|
1183
|
+
|
|
1184
|
+
const commitInputToHistory = (message: string) => {
|
|
1185
|
+
const normalized = message.trim();
|
|
1186
|
+
if (!normalized) {
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
setInputHistory((previous) => {
|
|
1191
|
+
if (previous.length > 0 && previous[previous.length - 1] === normalized) {
|
|
1192
|
+
return previous;
|
|
1193
|
+
}
|
|
1194
|
+
return [...previous, normalized];
|
|
1195
|
+
});
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
useEffect(() => {
|
|
1199
|
+
const restoredHistory = history ?? [];
|
|
1200
|
+
setHistoryState(restoredHistory);
|
|
1201
|
+
setTurns(mapHistoryToTurns(restoredHistory));
|
|
1202
|
+
setInputHistory(normalizeSessionUserHistory(restoredHistory));
|
|
1203
|
+
if (history !== undefined || !initialConversationId) {
|
|
1204
|
+
setIsHistoryLoading(false);
|
|
1205
|
+
}
|
|
1206
|
+
}, [history]);
|
|
1207
|
+
|
|
1208
|
+
useEffect(() => {
|
|
1209
|
+
setTurns(mapHistoryToTurns(historyState));
|
|
1210
|
+
}, [historyState]);
|
|
1211
|
+
|
|
1212
|
+
useEffect(() => {
|
|
1213
|
+
if (!initialConversationId || history !== undefined) {
|
|
1214
|
+
setIsHistoryLoading(false);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
let isActive = true;
|
|
1219
|
+
const hydrateSessionHistory = async () => {
|
|
1220
|
+
const result = await api.getAgentSession(initialConversationId);
|
|
1221
|
+
if (!isActive) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (!result.ok) {
|
|
1226
|
+
const lowerError = (result.error || "").toLowerCase();
|
|
1227
|
+
const notFound =
|
|
1228
|
+
result.status === 404 ||
|
|
1229
|
+
lowerError.includes("session not found") ||
|
|
1230
|
+
lowerError.includes("not found") ||
|
|
1231
|
+
lowerError.includes("does not exist");
|
|
1232
|
+
if (notFound) {
|
|
1233
|
+
setHistoryState([]);
|
|
1234
|
+
setTurns([]);
|
|
1235
|
+
setError("Could not load session history. This session could not be found on the backend. Resume is not available.");
|
|
1236
|
+
setIsResumeSessionUsable(false);
|
|
1237
|
+
setIsHistoryLoading(false);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
setError(
|
|
1241
|
+
`Could not load session history for ${initialConversationId}: ${result.error || "Unable to load conversation history."}` +
|
|
1242
|
+
" Proceeding without preloaded history."
|
|
1243
|
+
);
|
|
1244
|
+
setIsResumeSessionUsable(true);
|
|
1245
|
+
setIsHistoryLoading(false);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const restoredMessages = (result.data?.messages ?? [])
|
|
1250
|
+
.map((message) => {
|
|
1251
|
+
if (
|
|
1252
|
+
typeof message.role !== "string" ||
|
|
1253
|
+
typeof message.content !== "string"
|
|
1254
|
+
) {
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return {
|
|
1259
|
+
role: message.role,
|
|
1260
|
+
content: message.content,
|
|
1261
|
+
} as api.AgentChatHistoryItem;
|
|
1262
|
+
})
|
|
1263
|
+
.filter((message): message is api.AgentChatHistoryItem => message !== undefined);
|
|
1264
|
+
|
|
1265
|
+
setHistoryState(restoredMessages);
|
|
1266
|
+
setTurns(mapHistoryToTurns(restoredMessages));
|
|
1267
|
+
setInputHistory(normalizeSessionUserHistory(restoredMessages));
|
|
1268
|
+
|
|
1269
|
+
setIsHistoryLoading(false);
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
void hydrateSessionHistory();
|
|
1273
|
+
|
|
1274
|
+
return () => {
|
|
1275
|
+
isActive = false;
|
|
1276
|
+
};
|
|
1277
|
+
}, [initialConversationId, history]);
|
|
1117
1278
|
|
|
1118
1279
|
const runAutoTurn = async (rawMessage: string) => {
|
|
1280
|
+
if (isHistoryLoading) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1119
1283
|
const trimmed = rawMessage.trim();
|
|
1120
1284
|
if (!trimmed || isLoading) {
|
|
1121
1285
|
return;
|
|
1122
1286
|
}
|
|
1287
|
+
commitInputToHistory(trimmed);
|
|
1288
|
+
if (!isResumeSessionUsable && !allowSessionCreation) {
|
|
1289
|
+
setError("Unable to resume this session because the conversation record is unavailable.");
|
|
1290
|
+
setIsLoading(false);
|
|
1291
|
+
setStatusHint("Ready for next message.");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1123
1294
|
|
|
1124
1295
|
const nextHistory: api.AgentChatHistoryItem[] = [
|
|
1125
1296
|
...historyState,
|
|
1126
1297
|
{ role: "user", content: trimmed },
|
|
1127
1298
|
];
|
|
1128
1299
|
setHistoryState(nextHistory);
|
|
1129
|
-
|
|
1130
|
-
setInput("");
|
|
1300
|
+
setInputValue("");
|
|
1131
1301
|
setError("");
|
|
1132
1302
|
setIsLoading(true);
|
|
1133
1303
|
setStatusHint("Thinking...");
|
|
1134
1304
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1305
|
+
let activeConversationId = conversationId;
|
|
1306
|
+
|
|
1307
|
+
if (!activeConversationId && !allowSessionCreation) {
|
|
1308
|
+
setError("Unable to resume this session because no valid conversation id is available.");
|
|
1309
|
+
setIsLoading(false);
|
|
1310
|
+
setStatusHint("Ready for next message.");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (!activeConversationId) {
|
|
1315
|
+
setStatusHint("Creating conversation...");
|
|
1316
|
+
const createdSession = await api.createAgentSession({
|
|
1317
|
+
first_message: trimmed,
|
|
1318
|
+
title: trimmed.slice(0, 80) || "New agent session",
|
|
1319
|
+
});
|
|
1320
|
+
if (!createdSession.ok) {
|
|
1321
|
+
setError(`Failed to create conversation: ${createdSession.error || "Unknown error."}`);
|
|
1322
|
+
setIsLoading(false);
|
|
1323
|
+
setStatusHint("Ready for next message.");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const createdSessionId = createdSession.data?.id;
|
|
1327
|
+
if (!createdSessionId) {
|
|
1328
|
+
setError("Failed to create conversation: Missing conversation id from server response.");
|
|
1329
|
+
setIsLoading(false);
|
|
1330
|
+
setStatusHint("Ready for next message.");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
activeConversationId = createdSessionId;
|
|
1334
|
+
setConversationId(activeConversationId);
|
|
1335
|
+
persistConversation(activeConversationId);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const sendTurn = async (requestConversationId: string | undefined) =>
|
|
1339
|
+
api.agentChat({
|
|
1340
|
+
message: trimmed,
|
|
1341
|
+
...(requestConversationId ? { conversation_id: requestConversationId } : {}),
|
|
1342
|
+
...(nextHistory.length ? { history: nextHistory } : {}),
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
let result = await sendTurn(activeConversationId);
|
|
1346
|
+
const lowerError = (result.error || "").toLowerCase();
|
|
1347
|
+
const notFound =
|
|
1348
|
+
result.status === 404 ||
|
|
1349
|
+
lowerError.includes("session not found") ||
|
|
1350
|
+
lowerError.includes("not found") ||
|
|
1351
|
+
lowerError.includes("does not exist");
|
|
1140
1352
|
|
|
1141
1353
|
if (!result.ok) {
|
|
1142
|
-
if (
|
|
1354
|
+
if (lowerError.includes("failed to get session") && notFound) {
|
|
1355
|
+
if (!allowSessionCreation) {
|
|
1356
|
+
setError("Unable to resume this session. It is not available on the backend.");
|
|
1357
|
+
setIsResumeSessionUsable(false);
|
|
1358
|
+
} else {
|
|
1359
|
+
setError("Failed to resume session on the backend. Please start a new message to create a new session.");
|
|
1360
|
+
}
|
|
1361
|
+
} else if (
|
|
1143
1362
|
result.status === 403 &&
|
|
1144
|
-
|
|
1363
|
+
lowerError.includes("deep research mode")
|
|
1145
1364
|
) {
|
|
1146
1365
|
setError(
|
|
1147
1366
|
"Research mode requires a Pro subscription.\n" +
|
|
@@ -1151,29 +1370,121 @@ function AutoAgentInteractiveSession({
|
|
|
1151
1370
|
} else {
|
|
1152
1371
|
setError(result.error || "Agent request failed.");
|
|
1153
1372
|
}
|
|
1373
|
+
|
|
1374
|
+
setIsLoading(false);
|
|
1375
|
+
setStatusHint("Ready for next message.");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const data = result.data;
|
|
1380
|
+
if (!data) {
|
|
1381
|
+
setError("Agent request did not return a usable response.");
|
|
1154
1382
|
setIsLoading(false);
|
|
1155
1383
|
setStatusHint("Ready for next message.");
|
|
1156
1384
|
return;
|
|
1157
1385
|
}
|
|
1158
1386
|
|
|
1159
|
-
const answer =
|
|
1160
|
-
|
|
1387
|
+
const answer = data.answer?.trim() || "No response content yet.";
|
|
1388
|
+
setError("");
|
|
1389
|
+
const persistedConversationId = activeConversationId ?? data.conversation_id;
|
|
1390
|
+
if (persistedConversationId) {
|
|
1391
|
+
setConversationId(persistedConversationId);
|
|
1392
|
+
persistConversation(persistedConversationId);
|
|
1393
|
+
}
|
|
1161
1394
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1395
|
+
const assistantTurn: api.AgentChatHistoryItem = { role: "assistant", content: answer };
|
|
1396
|
+
if (persistedConversationId) {
|
|
1397
|
+
void api.appendSessionMessages(persistedConversationId, {
|
|
1398
|
+
messages: [
|
|
1399
|
+
{ role: "user", content: trimmed },
|
|
1400
|
+
{ role: "assistant", content: answer },
|
|
1401
|
+
],
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
setHistoryState((current) => [...current, assistantTurn]);
|
|
1165
1405
|
setStatusHint("Ready for next message.");
|
|
1166
1406
|
setIsLoading(false);
|
|
1167
1407
|
};
|
|
1168
1408
|
|
|
1409
|
+
useInput(
|
|
1410
|
+
(character, key) => {
|
|
1411
|
+
if (key.return) {
|
|
1412
|
+
void runAutoTurn(input);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (key.backspace || key.delete) {
|
|
1417
|
+
setInputValue(input.slice(0, -1));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (key.upArrow) {
|
|
1422
|
+
if (!inputHistory.length) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const nextIndex =
|
|
1426
|
+
inputHistoryIndex === -1 ? inputHistory.length - 1 : Math.max(0, inputHistoryIndex - 1);
|
|
1427
|
+
setInputHistoryIndex(nextIndex);
|
|
1428
|
+
setInput(inputHistory[nextIndex] ?? "");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (key.downArrow) {
|
|
1433
|
+
if (inputHistoryIndex === -1) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const nextIndex = inputHistoryIndex + 1;
|
|
1437
|
+
if (nextIndex >= inputHistory.length) {
|
|
1438
|
+
setInputHistoryIndex(-1);
|
|
1439
|
+
setInput("");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
setInputHistoryIndex(nextIndex);
|
|
1443
|
+
setInput(inputHistory[nextIndex] ?? "");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (
|
|
1448
|
+
key.tab ||
|
|
1449
|
+
key.leftArrow ||
|
|
1450
|
+
key.rightArrow ||
|
|
1451
|
+
key.escape ||
|
|
1452
|
+
key.ctrl ||
|
|
1453
|
+
key.meta
|
|
1454
|
+
) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (character) {
|
|
1459
|
+
setInputValue(input + character);
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
{ isActive: isRawModeSupported && !isLoading }
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
const persistConversation = (nextConversationId?: string) => {
|
|
1466
|
+
if (nextConversationId) {
|
|
1467
|
+
setLastAgentConversationId(nextConversationId);
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
return () => {
|
|
1473
|
+
persistConversation(conversationId);
|
|
1474
|
+
};
|
|
1475
|
+
}, [conversationId]);
|
|
1476
|
+
|
|
1169
1477
|
useEffect(() => {
|
|
1170
1478
|
if (!firstMessage || didSeedFirstMessage.current) {
|
|
1171
1479
|
return;
|
|
1172
1480
|
}
|
|
1481
|
+
if (isHistoryLoading) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1173
1484
|
|
|
1174
1485
|
didSeedFirstMessage.current = true;
|
|
1175
1486
|
void runAutoTurn(firstMessage);
|
|
1176
|
-
}, [firstMessage]);
|
|
1487
|
+
}, [firstMessage, isHistoryLoading]);
|
|
1177
1488
|
|
|
1178
1489
|
return (
|
|
1179
1490
|
<Box flexDirection="column">
|
|
@@ -1182,6 +1493,7 @@ function AutoAgentInteractiveSession({
|
|
|
1182
1493
|
Mode: {mode === "research" ? "research" : "standard"}{" "}
|
|
1183
1494
|
{conversationId ? `(conversation ${conversationId})` : "(new conversation)"}
|
|
1184
1495
|
</Text>
|
|
1496
|
+
{isHistoryLoading ? <Loading message="Loading conversation history..." /> : null}
|
|
1185
1497
|
<Box flexDirection="column" marginTop={1}>
|
|
1186
1498
|
{turns.map((entry, idx) => (
|
|
1187
1499
|
<Text key={`agent-turn-${idx}`} dimColor={entry.role === "user"}>
|
|
@@ -1194,8 +1506,9 @@ function AutoAgentInteractiveSession({
|
|
|
1194
1506
|
{isLoading ? null : (
|
|
1195
1507
|
<TextInput
|
|
1196
1508
|
value={input}
|
|
1197
|
-
|
|
1198
|
-
|
|
1509
|
+
focus={false}
|
|
1510
|
+
onChange={() => {}}
|
|
1511
|
+
onSubmit={() => {}}
|
|
1199
1512
|
/>
|
|
1200
1513
|
)}
|
|
1201
1514
|
</Box>
|
|
@@ -1209,93 +1522,439 @@ function AutoAgentInteractiveSession({
|
|
|
1209
1522
|
);
|
|
1210
1523
|
}
|
|
1211
1524
|
|
|
1212
|
-
function
|
|
1213
|
-
conversationId,
|
|
1214
|
-
history,
|
|
1525
|
+
export function AgentResumeCommand({
|
|
1215
1526
|
mode,
|
|
1216
1527
|
}: {
|
|
1217
|
-
conversationId?: string;
|
|
1218
|
-
history?: api.AgentChatHistoryItem[];
|
|
1219
1528
|
mode?: "standard" | "research";
|
|
1220
1529
|
}) {
|
|
1221
|
-
const [input, setInput] = useState("");
|
|
1222
|
-
const [isReady, setReady] = useState(false);
|
|
1223
|
-
const [message, setMessage] = useState("");
|
|
1224
1530
|
const { isRawModeSupported } = useStdin();
|
|
1225
|
-
const
|
|
1531
|
+
const [isReady, setReady] = useState(false);
|
|
1532
|
+
const [selectedSession, setSelectedSession] = useState<
|
|
1533
|
+
{
|
|
1534
|
+
id: string;
|
|
1535
|
+
} | undefined
|
|
1536
|
+
>();
|
|
1537
|
+
const [defaultSessionId] = useState(() => getLastAgentConversationId());
|
|
1538
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
1539
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
1540
|
+
const [sessions, setSessions] = useState<
|
|
1541
|
+
Array<{
|
|
1542
|
+
id: string;
|
|
1543
|
+
title: string;
|
|
1544
|
+
updated_at: string;
|
|
1545
|
+
is_archived: boolean;
|
|
1546
|
+
}>
|
|
1547
|
+
>([]);
|
|
1548
|
+
const [error, setError] = useState("");
|
|
1549
|
+
const [statusHint, setStatusHint] = useState("Loading sessions...");
|
|
1550
|
+
const [isSessionLoading, setSessionLoading] = useState(false);
|
|
1551
|
+
const [sessionError, setSessionError] = useState("");
|
|
1552
|
+
const [selectedSessionHistory, setSelectedSessionHistory] = useState<
|
|
1553
|
+
api.AgentChatHistoryItem[] | undefined
|
|
1554
|
+
>(undefined);
|
|
1226
1555
|
|
|
1227
1556
|
useEffect(() => {
|
|
1228
|
-
if (!
|
|
1557
|
+
if (!selectedSession?.id) {
|
|
1558
|
+
setSelectedSessionHistory(undefined);
|
|
1559
|
+
setSessionError("");
|
|
1229
1560
|
return;
|
|
1230
1561
|
}
|
|
1231
1562
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1563
|
+
let isActive = true;
|
|
1564
|
+
const loadSessionHistory = async () => {
|
|
1565
|
+
setSessionLoading(true);
|
|
1566
|
+
setSessionError("");
|
|
1235
1567
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1568
|
+
const result = await api.getAgentSession(selectedSession.id);
|
|
1569
|
+
if (!isActive) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (!result.ok) {
|
|
1574
|
+
setSessionError(
|
|
1575
|
+
`Could not load selected session ${selectedSession.id}: ${result.error || "Unknown error."}`
|
|
1576
|
+
);
|
|
1577
|
+
setSelectedSessionHistory(undefined);
|
|
1578
|
+
setSessionLoading(false);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const restoredMessages = (result.data?.messages ?? [])
|
|
1583
|
+
.map((message) => {
|
|
1584
|
+
if (typeof message?.role !== "string" || typeof message?.content !== "string") {
|
|
1585
|
+
return undefined;
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
role: message.role,
|
|
1589
|
+
content: message.content,
|
|
1590
|
+
} as api.AgentChatHistoryItem;
|
|
1591
|
+
})
|
|
1592
|
+
.filter((message): message is api.AgentChatHistoryItem => message !== undefined);
|
|
1593
|
+
|
|
1594
|
+
setSelectedSessionHistory(restoredMessages);
|
|
1595
|
+
setSessionLoading(false);
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
void loadSessionHistory();
|
|
1599
|
+
|
|
1600
|
+
return () => {
|
|
1601
|
+
isActive = false;
|
|
1602
|
+
setSessionLoading(false);
|
|
1603
|
+
};
|
|
1604
|
+
}, [selectedSession?.id]);
|
|
1605
|
+
|
|
1606
|
+
useEffect(() => {
|
|
1607
|
+
let isActive = true;
|
|
1608
|
+
const loadSessions = async () => {
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await api.listAgentSessions();
|
|
1611
|
+
if (!isActive) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (!result.ok) {
|
|
1615
|
+
setError(result.error || "Could not load agent sessions.");
|
|
1616
|
+
setStatusHint("");
|
|
1617
|
+
setReady(true);
|
|
1618
|
+
setSessions([]);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const remoteSessions = (result.data?.sessions ?? []).map((session) => ({
|
|
1622
|
+
id: session.id,
|
|
1623
|
+
title: session.title || "(untitled)",
|
|
1624
|
+
updated_at: session.updated_at || new Date().toISOString(),
|
|
1625
|
+
is_archived: session.is_archived,
|
|
1626
|
+
}));
|
|
1627
|
+
remoteSessions.sort((a, b) => {
|
|
1628
|
+
const aTime = Date.parse(a.updated_at);
|
|
1629
|
+
const bTime = Date.parse(b.updated_at);
|
|
1630
|
+
if (Number.isNaN(aTime) || Number.isNaN(bTime)) {
|
|
1631
|
+
return 0;
|
|
1632
|
+
}
|
|
1633
|
+
return bTime - aTime;
|
|
1634
|
+
});
|
|
1635
|
+
if (defaultSessionId) {
|
|
1636
|
+
const lastIndex = remoteSessions.findIndex((session) => session.id === defaultSessionId);
|
|
1637
|
+
if (lastIndex >= 0) {
|
|
1638
|
+
setHighlightIndex(lastIndex);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
setSessions(remoteSessions);
|
|
1642
|
+
setStatusHint("");
|
|
1643
|
+
setReady(true);
|
|
1644
|
+
return;
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
if (!isActive) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const message = err instanceof Error ? err.message : "Could not load agent sessions.";
|
|
1650
|
+
setError(message);
|
|
1651
|
+
setStatusHint("");
|
|
1652
|
+
setSessions([]);
|
|
1653
|
+
setReady(true);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
void loadSessions();
|
|
1657
|
+
|
|
1658
|
+
return () => {
|
|
1659
|
+
isActive = false;
|
|
1660
|
+
};
|
|
1661
|
+
}, []);
|
|
1662
|
+
|
|
1663
|
+
const normalizeSessionLabel = (session: {
|
|
1664
|
+
id: string;
|
|
1665
|
+
title: string;
|
|
1666
|
+
is_archived: boolean;
|
|
1667
|
+
updated_at: string;
|
|
1668
|
+
}) => `${session.title || "(untitled)"} ${session.is_archived ? "[archived] " : ""}(updated ${session.updated_at})`;
|
|
1669
|
+
|
|
1670
|
+
const matchingSessions = sessions.filter((session) => {
|
|
1671
|
+
const searchable = `${session.title} ${session.id}`.toLowerCase();
|
|
1672
|
+
const normalizedQuery = searchQuery.trim().toLowerCase();
|
|
1673
|
+
return normalizedQuery.length === 0 || searchable.includes(normalizedQuery);
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
useInput((_, key) => {
|
|
1677
|
+
if (!matchingSessions.length) return;
|
|
1678
|
+
if (key.upArrow) {
|
|
1679
|
+
setHighlightIndex((idx) => (idx === 0 ? matchingSessions.length - 1 : idx - 1));
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (key.downArrow) {
|
|
1683
|
+
setHighlightIndex((idx) => (idx === matchingSessions.length - 1 ? 0 : idx + 1));
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (key.return) {
|
|
1687
|
+
const selected = matchingSessions[highlightIndex];
|
|
1688
|
+
if (!selected) {
|
|
1689
|
+
setError("No matching sessions found.");
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
setError("");
|
|
1693
|
+
setSelectedSession({ id: selected.id });
|
|
1694
|
+
}
|
|
1695
|
+
}, { isActive: isRawModeSupported });
|
|
1238
1696
|
|
|
1239
1697
|
if (!isRawModeSupported) {
|
|
1240
1698
|
return (
|
|
1241
1699
|
<Box flexDirection="column">
|
|
1242
1700
|
<ErrorMessage error="Interactive input is not supported in this terminal." />
|
|
1243
1701
|
<Text>Use interactive mode for this environment:</Text>
|
|
1244
|
-
<Text dimColor>{` agent${mode === "research" ? " --mode research" : ""}`}</Text>
|
|
1245
1702
|
<Text dimColor> agent --help</Text>
|
|
1246
1703
|
</Box>
|
|
1247
1704
|
);
|
|
1248
1705
|
}
|
|
1249
1706
|
|
|
1707
|
+
if (error && !sessions.length) {
|
|
1708
|
+
return <ErrorMessage error={error} />;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1250
1711
|
if (!isReady) {
|
|
1712
|
+
return <Loading message={statusHint || "Loading sessions..."} />;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (!sessions.length) {
|
|
1251
1716
|
return (
|
|
1252
|
-
<
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
<Box>
|
|
1256
|
-
<Text color="cyan">> </Text>
|
|
1257
|
-
<TextInput
|
|
1258
|
-
value={input}
|
|
1259
|
-
onChange={setInput}
|
|
1260
|
-
onSubmit={(value) => {
|
|
1261
|
-
const trimmed = value.trim();
|
|
1262
|
-
if (!trimmed) {
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
setMessage(trimmed);
|
|
1266
|
-
setReady(true);
|
|
1267
|
-
}}
|
|
1268
|
-
/>
|
|
1269
|
-
</Box>
|
|
1270
|
-
<Text dimColor>Type Ctrl+C to cancel.</Text>
|
|
1271
|
-
</Box>
|
|
1717
|
+
<ErrorMessage
|
|
1718
|
+
error="No agent sessions found. Start a new conversation with `pioneer agent` and try again."
|
|
1719
|
+
/>
|
|
1272
1720
|
);
|
|
1273
1721
|
}
|
|
1274
1722
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1723
|
+
if (selectedSession?.id) {
|
|
1724
|
+
if (isSessionLoading) {
|
|
1725
|
+
return <Loading message={`Loading conversation ${selectedSession.id}...`} />;
|
|
1726
|
+
}
|
|
1727
|
+
if (sessionError) {
|
|
1728
|
+
return <ErrorMessage error={sessionError} />;
|
|
1729
|
+
}
|
|
1284
1730
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1731
|
+
return (
|
|
1732
|
+
<AutoAgentInteractiveSession
|
|
1733
|
+
conversationId={selectedSession.id}
|
|
1734
|
+
mode={mode ?? "standard"}
|
|
1735
|
+
history={selectedSessionHistory}
|
|
1736
|
+
allowSessionCreation={false}
|
|
1737
|
+
/>
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1288
1740
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1741
|
+
return (
|
|
1742
|
+
<Box flexDirection="column">
|
|
1743
|
+
<Text bold>Agent sessions:</Text>
|
|
1744
|
+
<Text>
|
|
1745
|
+
{defaultSessionId ? `Last session: ${defaultSessionId}. ` : ""}
|
|
1746
|
+
Type to filter. Use ↑/↓ to navigate and Enter to select.
|
|
1747
|
+
</Text>
|
|
1748
|
+
<Text dimColor>Search title or session ID.</Text>
|
|
1749
|
+
<Text> </Text>
|
|
1750
|
+
{matchingSessions.length === 0 ? (
|
|
1751
|
+
<Text color="yellow">No sessions match "{searchQuery}".</Text>
|
|
1752
|
+
) : (
|
|
1753
|
+
matchingSessions.slice(0, 12).map((session, index) => {
|
|
1754
|
+
const isSelected = index === highlightIndex;
|
|
1755
|
+
return (
|
|
1756
|
+
<Text key={session.id} color={isSelected ? "cyan" : undefined}>
|
|
1757
|
+
{`${isSelected ? ">" : " "} ${normalizeSessionLabel(session)} (${session.id})`}
|
|
1758
|
+
</Text>
|
|
1759
|
+
);
|
|
1760
|
+
})
|
|
1761
|
+
)}
|
|
1762
|
+
<Box marginTop={1}>
|
|
1763
|
+
<Text color="cyan">> </Text>
|
|
1764
|
+
<TextInput
|
|
1765
|
+
value={searchQuery}
|
|
1766
|
+
onChange={setSearchQuery}
|
|
1767
|
+
onSubmit={(rawValue) => {
|
|
1768
|
+
const trimmed = rawValue.trim().toLowerCase();
|
|
1769
|
+
if (!trimmed && !matchingSessions.length) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const exactMatch = matchingSessions.find(
|
|
1773
|
+
(session) =>
|
|
1774
|
+
session.id.toLowerCase() === trimmed ||
|
|
1775
|
+
session.title.toLowerCase() === trimmed
|
|
1776
|
+
);
|
|
1777
|
+
const selected = exactMatch ?? matchingSessions[highlightIndex];
|
|
1778
|
+
if (!selected?.id) {
|
|
1779
|
+
setError("No matching session found.");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
setError("");
|
|
1783
|
+
setSelectedSession({ id: selected.id });
|
|
1784
|
+
}}
|
|
1785
|
+
/>
|
|
1786
|
+
</Box>
|
|
1787
|
+
{error && <ErrorMessage error={error} />}
|
|
1788
|
+
</Box>
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function AgentInteractivePrompt({
|
|
1793
|
+
conversationId,
|
|
1794
|
+
history,
|
|
1795
|
+
mode,
|
|
1796
|
+
allowSessionCreation = true,
|
|
1797
|
+
}: {
|
|
1798
|
+
conversationId?: string;
|
|
1799
|
+
history?: api.AgentChatHistoryItem[];
|
|
1800
|
+
mode?: "standard" | "research";
|
|
1801
|
+
allowSessionCreation?: boolean;
|
|
1802
|
+
}) {
|
|
1803
|
+
const [input, setInput] = useState("");
|
|
1804
|
+
const [inputHistory, setInputHistory] = useState<string[]>([]);
|
|
1805
|
+
const [inputHistoryIndex, setInputHistoryIndex] = useState(-1);
|
|
1806
|
+
const [isReady, setReady] = useState(false);
|
|
1807
|
+
const [message, setMessage] = useState("");
|
|
1808
|
+
const { isRawModeSupported } = useStdin();
|
|
1809
|
+
const shouldExitImmediately = !isRawModeSupported;
|
|
1810
|
+
|
|
1811
|
+
const commitInputToHistory = (value: string) => {
|
|
1812
|
+
const normalized = value.trim();
|
|
1813
|
+
if (!normalized) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
setInputHistory((previous) => {
|
|
1817
|
+
if (previous.length > 0 && previous[previous.length - 1] === normalized) {
|
|
1818
|
+
return previous;
|
|
1819
|
+
}
|
|
1820
|
+
return [...previous, normalized];
|
|
1821
|
+
});
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
const setInputValue = (nextValue: string) => {
|
|
1825
|
+
setInput(nextValue);
|
|
1826
|
+
setInputHistoryIndex(-1);
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
useInput(
|
|
1830
|
+
(character, key) => {
|
|
1831
|
+
if (key.return) {
|
|
1832
|
+
const trimmed = input.trim();
|
|
1833
|
+
if (!trimmed) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
commitInputToHistory(trimmed);
|
|
1837
|
+
setMessage(trimmed);
|
|
1838
|
+
setReady(true);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (key.backspace || key.delete) {
|
|
1843
|
+
setInputValue(input.slice(0, -1));
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (key.upArrow) {
|
|
1848
|
+
if (!inputHistory.length) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const nextIndex =
|
|
1852
|
+
inputHistoryIndex === -1 ? inputHistory.length - 1 : Math.max(0, inputHistoryIndex - 1);
|
|
1853
|
+
setInputHistoryIndex(nextIndex);
|
|
1854
|
+
setInput(inputHistory[nextIndex] ?? "");
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (key.downArrow) {
|
|
1859
|
+
if (inputHistoryIndex === -1) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const nextIndex = inputHistoryIndex + 1;
|
|
1863
|
+
if (nextIndex >= inputHistory.length) {
|
|
1864
|
+
setInputHistoryIndex(-1);
|
|
1865
|
+
setInput("");
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
setInputHistoryIndex(nextIndex);
|
|
1869
|
+
setInput(inputHistory[nextIndex] ?? "");
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
if (
|
|
1874
|
+
key.tab ||
|
|
1875
|
+
key.leftArrow ||
|
|
1876
|
+
key.rightArrow ||
|
|
1877
|
+
key.escape ||
|
|
1878
|
+
key.ctrl ||
|
|
1879
|
+
key.meta
|
|
1880
|
+
) {
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (character) {
|
|
1885
|
+
setInputValue(input + character);
|
|
1886
|
+
}
|
|
1887
|
+
},
|
|
1888
|
+
{ isActive: isRawModeSupported && !isReady }
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
useEffect(() => {
|
|
1892
|
+
if (!shouldExitImmediately) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const timeout = setTimeout(() => {
|
|
1897
|
+
process.exit(0);
|
|
1898
|
+
}, 75);
|
|
1899
|
+
|
|
1900
|
+
return () => clearTimeout(timeout);
|
|
1901
|
+
}, [shouldExitImmediately]);
|
|
1902
|
+
|
|
1903
|
+
if (!isRawModeSupported) {
|
|
1904
|
+
return (
|
|
1905
|
+
<Box flexDirection="column">
|
|
1906
|
+
<ErrorMessage error="Interactive input is not supported in this terminal." />
|
|
1907
|
+
<Text>Use interactive mode for this environment:</Text>
|
|
1908
|
+
<Text dimColor>{` agent${mode === "research" ? " --mode research" : ""}`}</Text>
|
|
1909
|
+
<Text dimColor> agent --help</Text>
|
|
1910
|
+
</Box>
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (!isReady) {
|
|
1915
|
+
return (
|
|
1916
|
+
<Box flexDirection="column">
|
|
1917
|
+
<Text bold>Agent mode selected.</Text>
|
|
1918
|
+
<Text>Type your message and press enter to start:</Text>
|
|
1919
|
+
<Box>
|
|
1920
|
+
<Text color="cyan">> </Text>
|
|
1921
|
+
<TextInput
|
|
1922
|
+
value={input}
|
|
1923
|
+
focus={false}
|
|
1924
|
+
onChange={() => {}}
|
|
1925
|
+
onSubmit={() => {}}
|
|
1926
|
+
/>
|
|
1927
|
+
</Box>
|
|
1928
|
+
<Text dimColor>Type Ctrl+C to cancel.</Text>
|
|
1929
|
+
</Box>
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
return (
|
|
1934
|
+
<AutoAgentInteractiveSession
|
|
1935
|
+
conversationId={conversationId}
|
|
1936
|
+
history={history}
|
|
1937
|
+
mode={mode === "research" ? "research" : "standard"}
|
|
1938
|
+
firstMessage={message}
|
|
1939
|
+
allowSessionCreation={allowSessionCreation}
|
|
1940
|
+
/>
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1945
|
+
// Interactive Model Create Selector
|
|
1946
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1947
|
+
|
|
1948
|
+
function ModelCreateInteractive({
|
|
1949
|
+
name,
|
|
1950
|
+
icon,
|
|
1951
|
+
repo,
|
|
1952
|
+
description,
|
|
1953
|
+
example,
|
|
1954
|
+
}: {
|
|
1955
|
+
name?: string;
|
|
1956
|
+
icon?: string;
|
|
1957
|
+
repo?: string;
|
|
1299
1958
|
description?: string;
|
|
1300
1959
|
example?: CreateProjectExample;
|
|
1301
1960
|
}) {
|
|
@@ -1304,6 +1963,7 @@ function ModelCreateInteractive({
|
|
|
1304
1963
|
const [loading, setLoading] = useState(true);
|
|
1305
1964
|
const [error, setError] = useState("");
|
|
1306
1965
|
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
|
|
1966
|
+
const { isRawModeSupported } = useStdin();
|
|
1307
1967
|
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
1308
1968
|
|
|
1309
1969
|
useEffect(() => {
|
|
@@ -1352,104 +2012,1311 @@ function ModelCreateInteractive({
|
|
|
1352
2012
|
}
|
|
1353
2013
|
}, [highlightIndex, topMatches.length]);
|
|
1354
2014
|
|
|
1355
|
-
useInput(
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
2015
|
+
useInput(
|
|
2016
|
+
(_, key) => {
|
|
2017
|
+
if (!topMatches.length) return;
|
|
2018
|
+
if (key.upArrow) {
|
|
2019
|
+
setHighlightIndex((idx) => (idx === 0 ? topMatches.length - 1 : idx - 1));
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (key.downArrow) {
|
|
2023
|
+
setHighlightIndex((idx) => (idx === topMatches.length - 1 ? 0 : idx + 1));
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
if (key.return) {
|
|
2027
|
+
const resolvedModel = resolveModelId(query);
|
|
2028
|
+
if (!resolvedModel) {
|
|
2029
|
+
setError("No matching models found. Refine your search and try again.");
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
setSelectedModelId(resolvedModel);
|
|
2033
|
+
}
|
|
2034
|
+
},
|
|
2035
|
+
{ isActive: isRawModeSupported }
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
if (error) {
|
|
2039
|
+
return (
|
|
2040
|
+
<Box flexDirection="column">
|
|
2041
|
+
<ErrorMessage error={error} />
|
|
2042
|
+
<Text> </Text>
|
|
2043
|
+
<Text dimColor>Try again with --model explicitly, or check your API connectivity.</Text>
|
|
2044
|
+
</Box>
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (selectedModelId) {
|
|
2049
|
+
return (
|
|
2050
|
+
<ApiCommand
|
|
2051
|
+
action={() =>
|
|
2052
|
+
api.createProject({
|
|
2053
|
+
name: name ?? selectedModelId,
|
|
2054
|
+
...(icon ? { icon } : {}),
|
|
2055
|
+
...(repo ? { repo } : {}),
|
|
2056
|
+
...(description ? { description } : {}),
|
|
2057
|
+
active_model_id: selectedModelId,
|
|
2058
|
+
selected_model_id: selectedModelId,
|
|
2059
|
+
...(example ? { example } : {}),
|
|
2060
|
+
})
|
|
2061
|
+
}
|
|
2062
|
+
successMessage="Model entry created"
|
|
2063
|
+
/>
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (loading) {
|
|
2068
|
+
return <Loading message="Loading supported models..." />;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
return (
|
|
2072
|
+
<Box flexDirection="column">
|
|
2073
|
+
<Text bold>Choose a base model for this entry</Text>
|
|
2074
|
+
<Text>Type to filter. Use ↑/↓ to navigate and Enter to select.</Text>
|
|
2075
|
+
<Text dimColor>Type any part of model id, name, label, or description.</Text>
|
|
2076
|
+
<Text> </Text>
|
|
2077
|
+
<TextInput
|
|
2078
|
+
value={query}
|
|
2079
|
+
onChange={(value) => setQuery(value)}
|
|
2080
|
+
onSubmit={(value) => {
|
|
2081
|
+
const resolvedModel = resolveModelId(value);
|
|
2082
|
+
if (!resolvedModel) {
|
|
2083
|
+
setError("No matching models found. Refine your search and try again.");
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
setSelectedModelId(resolvedModel);
|
|
2087
|
+
}}
|
|
2088
|
+
placeholder="e.g. qwen/qwen3-8b"
|
|
2089
|
+
/>
|
|
2090
|
+
<Text> </Text>
|
|
2091
|
+
{topMatches.length === 0 ? (
|
|
2092
|
+
<Text dimColor>No matching models found.</Text>
|
|
2093
|
+
) : (
|
|
2094
|
+
<Box flexDirection="column">
|
|
2095
|
+
{topMatches.map((model, index) => {
|
|
2096
|
+
const suffix = [model.label, model.task_type].filter(Boolean).join(" · ");
|
|
2097
|
+
const modelLine = `${model.id}${suffix ? ` (${suffix})` : ""}`;
|
|
2098
|
+
const isHighlighted = index === highlightIndex;
|
|
2099
|
+
return (
|
|
2100
|
+
<Text key={model.id} color={isHighlighted ? "cyan" : undefined} bold={isHighlighted}>
|
|
2101
|
+
{isHighlighted ? "▶ " : " "}
|
|
2102
|
+
{modelLine}
|
|
2103
|
+
</Text>
|
|
2104
|
+
);
|
|
2105
|
+
})}
|
|
2106
|
+
</Box>
|
|
2107
|
+
)}
|
|
2108
|
+
<Text> </Text>
|
|
2109
|
+
<Text dimColor>Press Enter to select the highlighted model.</Text>
|
|
2110
|
+
<Text dimColor> </Text>
|
|
2111
|
+
<Text dimColor>Tip: You can still run {"model endpoints create --model \"<base-model-id>\""} for exact model ids.</Text>
|
|
2112
|
+
</Box>
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2117
|
+
// Job ID Resolver
|
|
2118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2119
|
+
|
|
2120
|
+
function WithResolvedJobId({
|
|
2121
|
+
rawId,
|
|
2122
|
+
render,
|
|
2123
|
+
}: {
|
|
2124
|
+
rawId: string;
|
|
2125
|
+
render: (jobId: string) => React.ReactElement;
|
|
2126
|
+
}) {
|
|
2127
|
+
const initiallyResolved = isUuid(rawId);
|
|
2128
|
+
const [state, setState] = useState<"resolving" | "resolved" | "error">(
|
|
2129
|
+
initiallyResolved ? "resolved" : "resolving"
|
|
2130
|
+
);
|
|
2131
|
+
const [resolvedId, setResolvedId] = useState(initiallyResolved ? rawId : "");
|
|
2132
|
+
const [error, setError] = useState("");
|
|
2133
|
+
|
|
2134
|
+
useEffect(() => {
|
|
2135
|
+
if (initiallyResolved) {
|
|
1363
2136
|
return;
|
|
1364
2137
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
2138
|
+
let isActive = true;
|
|
2139
|
+
(async () => {
|
|
2140
|
+
const result = await api.listJobs();
|
|
2141
|
+
if (!isActive) return;
|
|
2142
|
+
if (!result.ok) {
|
|
2143
|
+
setError(result.error ?? "Failed to look up training jobs.");
|
|
2144
|
+
setState("error");
|
|
1369
2145
|
return;
|
|
1370
2146
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
2147
|
+
const jobs = result.data?.training_jobs ?? [];
|
|
2148
|
+
const norm = rawId.toLowerCase();
|
|
2149
|
+
const matches = jobs.filter((job) =>
|
|
2150
|
+
typeof job.id === "string" && job.id.toLowerCase().startsWith(norm)
|
|
2151
|
+
);
|
|
2152
|
+
if (matches.length === 0) {
|
|
2153
|
+
setError(
|
|
2154
|
+
`No training job matches "${rawId}". Run \`pioneer job list\` to see available IDs.`
|
|
2155
|
+
);
|
|
2156
|
+
setState("error");
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
if (matches.length > 1) {
|
|
2160
|
+
const preview = matches
|
|
2161
|
+
.slice(0, 5)
|
|
2162
|
+
.map((j) => `${j.id} (${j.model_name ?? "—"})`)
|
|
2163
|
+
.join(", ");
|
|
2164
|
+
setError(
|
|
2165
|
+
`Ambiguous job id "${rawId}". Matches: ${preview}${matches.length > 5 ? "…" : ""}. Use a longer prefix or the full UUID.`
|
|
2166
|
+
);
|
|
2167
|
+
setState("error");
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
setResolvedId(matches[0]!.id!);
|
|
2171
|
+
setState("resolved");
|
|
2172
|
+
})();
|
|
2173
|
+
|
|
2174
|
+
return () => {
|
|
2175
|
+
isActive = false;
|
|
2176
|
+
};
|
|
2177
|
+
}, [rawId, initiallyResolved]);
|
|
2178
|
+
|
|
2179
|
+
if (state === "resolving") {
|
|
2180
|
+
return <Loading message={`Resolving job id ${rawId}...`} />;
|
|
2181
|
+
}
|
|
2182
|
+
if (state === "error") {
|
|
2183
|
+
return <ErrorMessage error={error} />;
|
|
2184
|
+
}
|
|
2185
|
+
return render(resolvedId);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2189
|
+
// Job List Command (tabular output)
|
|
2190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2191
|
+
|
|
2192
|
+
function shortJobId(id?: string): string {
|
|
2193
|
+
if (!id) return "—";
|
|
2194
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function shortBaseModel(baseModel?: string): string {
|
|
2198
|
+
if (!baseModel) return "—";
|
|
2199
|
+
const parts = baseModel.split("/");
|
|
2200
|
+
return parts[parts.length - 1] || baseModel;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function relativeTime(iso?: string): string {
|
|
2204
|
+
if (!iso) return "—";
|
|
2205
|
+
const ts = Date.parse(iso);
|
|
2206
|
+
if (Number.isNaN(ts)) return iso;
|
|
2207
|
+
const diffMs = Date.now() - ts;
|
|
2208
|
+
const seconds = Math.round(diffMs / 1000);
|
|
2209
|
+
if (seconds < 60) return `${Math.max(seconds, 0)}s ago`;
|
|
2210
|
+
const minutes = Math.round(seconds / 60);
|
|
2211
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
2212
|
+
const hours = Math.round(minutes / 60);
|
|
2213
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2214
|
+
const days = Math.round(hours / 24);
|
|
2215
|
+
if (days < 30) return `${days}d ago`;
|
|
2216
|
+
const months = Math.round(days / 30);
|
|
2217
|
+
if (months < 12) return `${months}mo ago`;
|
|
2218
|
+
const years = Math.round(days / 365);
|
|
2219
|
+
return `${years}y ago`;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function statusColor(status?: string): string | undefined {
|
|
2223
|
+
const normalized = (status ?? "").toLowerCase();
|
|
2224
|
+
if (normalized === "complete" || normalized === "completed" || normalized === "succeeded") {
|
|
2225
|
+
return "green";
|
|
2226
|
+
}
|
|
2227
|
+
if (normalized === "failed" || normalized === "error" || normalized === "cancelled" || normalized === "canceled") {
|
|
2228
|
+
return "red";
|
|
2229
|
+
}
|
|
2230
|
+
if (normalized === "running" || normalized === "training" || normalized === "in_progress") {
|
|
2231
|
+
return "cyan";
|
|
2232
|
+
}
|
|
2233
|
+
if (normalized === "pending" || normalized === "queued" || normalized === "starting") {
|
|
2234
|
+
return "yellow";
|
|
2235
|
+
}
|
|
2236
|
+
return undefined;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function padCell(value: string, width: number): string {
|
|
2240
|
+
if (value.length === width) return value;
|
|
2241
|
+
if (value.length > width) {
|
|
2242
|
+
if (width <= 1) return value.slice(0, width);
|
|
2243
|
+
return `${value.slice(0, Math.max(width - 1, 1))}…`;
|
|
2244
|
+
}
|
|
2245
|
+
return value + " ".repeat(width - value.length);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
interface JobRow {
|
|
2249
|
+
id: string;
|
|
2250
|
+
model: string;
|
|
2251
|
+
baseModel: string;
|
|
2252
|
+
task: string;
|
|
2253
|
+
status: string;
|
|
2254
|
+
created: string;
|
|
2255
|
+
[key: string]: string;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function jobsToRows(jobs: api.TrainingJob[]): JobRow[] {
|
|
2259
|
+
return jobs.map((job) => ({
|
|
2260
|
+
id: shortJobId(job.id),
|
|
2261
|
+
model: job.model_name?.trim() || "—",
|
|
2262
|
+
baseModel: shortBaseModel(job.base_model),
|
|
2263
|
+
task: job.task_type?.trim() || "—",
|
|
2264
|
+
status: job.status?.trim() || "—",
|
|
2265
|
+
created: relativeTime(job.created_at),
|
|
2266
|
+
}));
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2270
|
+
// Reusable Table + DetailView primitives
|
|
2271
|
+
// Used by: job list/get, dataset list/get, model base-models, model endpoints get,
|
|
2272
|
+
// model endpoints quality-metrics, model artifacts download.
|
|
2273
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2274
|
+
|
|
2275
|
+
interface TableColumn<TRow> {
|
|
2276
|
+
key: keyof TRow & string;
|
|
2277
|
+
header: string;
|
|
2278
|
+
minWidth: number;
|
|
2279
|
+
maxWidth: number;
|
|
2280
|
+
flexible?: boolean;
|
|
2281
|
+
color?: (row: TRow) => string | undefined;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function computeTableWidths<TRow extends Record<string, string>>(
|
|
2285
|
+
columns: TableColumn<TRow>[],
|
|
2286
|
+
rows: TRow[],
|
|
2287
|
+
terminalWidth: number
|
|
2288
|
+
): number[] {
|
|
2289
|
+
const desired = columns.map((column) => {
|
|
2290
|
+
const headerLength = column.header.length;
|
|
2291
|
+
const dataLength = rows.reduce(
|
|
2292
|
+
(max, row) => Math.max(max, ((row[column.key] as string | undefined) ?? "").length),
|
|
2293
|
+
0
|
|
2294
|
+
);
|
|
2295
|
+
const want = Math.max(headerLength, dataLength);
|
|
2296
|
+
return Math.max(column.minWidth, Math.min(column.maxWidth, want));
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
const separators = (columns.length - 1) * 2;
|
|
2300
|
+
const available = Math.max(40, terminalWidth - 1);
|
|
2301
|
+
const total = desired.reduce((sum, w) => sum + w, 0) + separators;
|
|
2302
|
+
|
|
2303
|
+
if (total <= available) return desired;
|
|
2304
|
+
|
|
2305
|
+
let overflow = total - available;
|
|
2306
|
+
const flexIndices = columns
|
|
2307
|
+
.map((column, idx) => (column.flexible ? idx : -1))
|
|
2308
|
+
.filter((idx) => idx >= 0);
|
|
2309
|
+
|
|
2310
|
+
while (overflow > 0) {
|
|
2311
|
+
let trimmedThisPass = false;
|
|
2312
|
+
for (const idx of flexIndices) {
|
|
2313
|
+
if (overflow <= 0) break;
|
|
2314
|
+
if (desired[idx]! > columns[idx]!.minWidth) {
|
|
2315
|
+
desired[idx] = desired[idx]! - 1;
|
|
2316
|
+
overflow -= 1;
|
|
2317
|
+
trimmedThisPass = true;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
if (!trimmedThisPass) break;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
return desired;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function useTerminalWidth(): number {
|
|
2327
|
+
const { stdout } = useStdout();
|
|
2328
|
+
const [width, setWidth] = useState(() => {
|
|
2329
|
+
const cols = stdout?.columns;
|
|
2330
|
+
if (typeof cols === "number" && cols > 0) return cols;
|
|
2331
|
+
const env = parseInt(process.env.COLUMNS ?? "", 10);
|
|
2332
|
+
return Number.isFinite(env) && env > 0 ? env : 80;
|
|
2333
|
+
});
|
|
2334
|
+
useEffect(() => {
|
|
2335
|
+
if (!stdout) return;
|
|
2336
|
+
const handler = () => {
|
|
2337
|
+
if (typeof stdout.columns === "number" && stdout.columns > 0) {
|
|
2338
|
+
setWidth(stdout.columns);
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
stdout.on("resize", handler);
|
|
2342
|
+
return () => {
|
|
2343
|
+
stdout.off("resize", handler);
|
|
2344
|
+
};
|
|
2345
|
+
}, [stdout]);
|
|
2346
|
+
return width;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
function DataTable<TRow extends Record<string, string>>({
|
|
2350
|
+
columns,
|
|
2351
|
+
rows,
|
|
2352
|
+
footer,
|
|
2353
|
+
}: {
|
|
2354
|
+
columns: TableColumn<TRow>[];
|
|
2355
|
+
rows: TRow[];
|
|
2356
|
+
footer?: string;
|
|
2357
|
+
}) {
|
|
2358
|
+
const terminalWidth = useTerminalWidth();
|
|
2359
|
+
const widths = computeTableWidths(columns, rows, terminalWidth);
|
|
2360
|
+
|
|
2361
|
+
const headerCells = columns.map((column, idx) =>
|
|
2362
|
+
padCell(column.header, widths[idx]!)
|
|
2363
|
+
);
|
|
2364
|
+
|
|
2365
|
+
return (
|
|
2366
|
+
<Box flexDirection="column">
|
|
2367
|
+
<Text bold dimColor>{headerCells.join(" ")}</Text>
|
|
2368
|
+
{rows.map((row, rowIdx) => {
|
|
2369
|
+
const cells = columns.map((column, idx) => {
|
|
2370
|
+
const raw = (row[column.key] as string | undefined) ?? "";
|
|
2371
|
+
return padCell(String(raw), widths[idx]!);
|
|
2372
|
+
});
|
|
2373
|
+
return (
|
|
2374
|
+
<Text key={rowIdx}>
|
|
2375
|
+
{columns.map((column, idx) => {
|
|
2376
|
+
const cell = cells[idx]!;
|
|
2377
|
+
const color = column.color?.(row);
|
|
2378
|
+
return (
|
|
2379
|
+
<React.Fragment key={String(column.key)}>
|
|
2380
|
+
{idx > 0 ? " " : ""}
|
|
2381
|
+
{color ? <Text color={color}>{cell}</Text> : cell}
|
|
2382
|
+
</React.Fragment>
|
|
2383
|
+
);
|
|
2384
|
+
})}
|
|
2385
|
+
</Text>
|
|
2386
|
+
);
|
|
2387
|
+
})}
|
|
2388
|
+
{footer ? (
|
|
2389
|
+
<Box marginTop={1}>
|
|
2390
|
+
<Text dimColor>{footer}</Text>
|
|
2391
|
+
</Box>
|
|
2392
|
+
) : null}
|
|
2393
|
+
</Box>
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
interface DetailRow {
|
|
2398
|
+
label: string;
|
|
2399
|
+
value: string;
|
|
2400
|
+
valueColor?: string;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function DetailView({
|
|
2404
|
+
rows,
|
|
2405
|
+
footer,
|
|
2406
|
+
}: {
|
|
2407
|
+
rows: DetailRow[];
|
|
2408
|
+
footer?: string;
|
|
2409
|
+
}) {
|
|
2410
|
+
if (rows.length === 0) {
|
|
2411
|
+
return <Text dimColor>No data.</Text>;
|
|
2412
|
+
}
|
|
2413
|
+
// Render single-record details through the same DataTable primitive used for
|
|
2414
|
+
// list views, so `<thing> get` and `<thing> list` share the exact same
|
|
2415
|
+
// visual style (uppercase headers, column padding, ellipsizing).
|
|
2416
|
+
const tableRows = rows.map((row) => ({
|
|
2417
|
+
field: row.label.toUpperCase(),
|
|
2418
|
+
value: row.value,
|
|
2419
|
+
__valueColor: row.valueColor ?? "",
|
|
2420
|
+
}));
|
|
2421
|
+
const columns: TableColumn<(typeof tableRows)[number]>[] = [
|
|
2422
|
+
{ key: "field", header: "FIELD", minWidth: 6, maxWidth: 24 },
|
|
2423
|
+
{
|
|
2424
|
+
key: "value",
|
|
2425
|
+
header: "VALUE",
|
|
2426
|
+
minWidth: 10,
|
|
2427
|
+
maxWidth: 80,
|
|
2428
|
+
flexible: true,
|
|
2429
|
+
color: (row) => row.__valueColor || undefined,
|
|
2430
|
+
},
|
|
2431
|
+
];
|
|
2432
|
+
return <DataTable columns={columns} rows={tableRows} footer={footer} />;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
const JOB_COLUMNS: TableColumn<JobRow>[] = [
|
|
2436
|
+
{ key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
|
|
2437
|
+
{ key: "model", header: "MODEL", minWidth: 8, maxWidth: 32, flexible: true },
|
|
2438
|
+
{ key: "baseModel", header: "BASE MODEL", minWidth: 10, maxWidth: 30, flexible: true },
|
|
2439
|
+
{ key: "task", header: "TASK", minWidth: 4, maxWidth: 14, flexible: true },
|
|
2440
|
+
{ key: "status", header: "STATUS", minWidth: 6, maxWidth: 10, color: (row) => statusColor(row.status) },
|
|
2441
|
+
{ key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
|
|
2442
|
+
];
|
|
2443
|
+
|
|
2444
|
+
export function JobListCommand() {
|
|
2445
|
+
const { exit } = useApp();
|
|
2446
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2447
|
+
const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
|
|
2448
|
+
const [error, setError] = useState("");
|
|
2449
|
+
|
|
2450
|
+
useEffect(() => {
|
|
2451
|
+
(async () => {
|
|
2452
|
+
const result = await api.listJobs();
|
|
2453
|
+
if (!result.ok) {
|
|
2454
|
+
setError(result.error ?? "Failed to load training jobs.");
|
|
2455
|
+
setState("error");
|
|
2456
|
+
} else {
|
|
2457
|
+
setJobs(result.data?.training_jobs ?? []);
|
|
2458
|
+
setState("done");
|
|
2459
|
+
}
|
|
2460
|
+
setTimeout(() => exit(), 500);
|
|
2461
|
+
})();
|
|
2462
|
+
}, [exit]);
|
|
2463
|
+
|
|
2464
|
+
if (state === "loading") return <Loading message="Loading training jobs..." />;
|
|
2465
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2466
|
+
if (jobs.length === 0) {
|
|
2467
|
+
return (
|
|
2468
|
+
<Box flexDirection="column">
|
|
2469
|
+
<Text>No training jobs found.</Text>
|
|
2470
|
+
<Text dimColor>Create one with `pioneer agent` — it will guide you through picking a base model and datasets.</Text>
|
|
2471
|
+
</Box>
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
return (
|
|
2476
|
+
<DataTable
|
|
2477
|
+
columns={JOB_COLUMNS}
|
|
2478
|
+
rows={jobsToRows(jobs)}
|
|
2479
|
+
footer={`${jobs.length} job${jobs.length === 1 ? "" : "s"} · the short ID prefix above is enough for \`pioneer job get/logs/delete\` · use \`--json\` for raw JSON`}
|
|
2480
|
+
/>
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2485
|
+
// Job Get Command (single-job key/value layout)
|
|
2486
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2487
|
+
|
|
2488
|
+
function formatTimestamp(iso?: string): string {
|
|
2489
|
+
if (!iso) return "—";
|
|
2490
|
+
const ts = Date.parse(iso);
|
|
2491
|
+
if (Number.isNaN(ts)) return iso;
|
|
2492
|
+
const date = new Date(ts);
|
|
2493
|
+
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
2494
|
+
const yyyy = date.getUTCFullYear();
|
|
2495
|
+
const mm = pad(date.getUTCMonth() + 1);
|
|
2496
|
+
const dd = pad(date.getUTCDate());
|
|
2497
|
+
const hh = pad(date.getUTCHours());
|
|
2498
|
+
const mi = pad(date.getUTCMinutes());
|
|
2499
|
+
const ss = pad(date.getUTCSeconds());
|
|
2500
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss} UTC (${relativeTime(iso)})`;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
function formatDuration(startIso?: string, endIso?: string): string {
|
|
2504
|
+
if (!startIso || !endIso) return "—";
|
|
2505
|
+
const start = Date.parse(startIso);
|
|
2506
|
+
const end = Date.parse(endIso);
|
|
2507
|
+
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return "—";
|
|
2508
|
+
const seconds = Math.floor((end - start) / 1000);
|
|
2509
|
+
const days = Math.floor(seconds / 86400);
|
|
2510
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
2511
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
2512
|
+
const remSeconds = seconds % 60;
|
|
2513
|
+
const parts: string[] = [];
|
|
2514
|
+
if (days) parts.push(`${days}d`);
|
|
2515
|
+
if (hours) parts.push(`${hours}h`);
|
|
2516
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
2517
|
+
if (!parts.length || remSeconds) parts.push(`${remSeconds}s`);
|
|
2518
|
+
return parts.join(" ");
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function formatDatasetList(
|
|
2522
|
+
datasets?: Array<{ name?: string; version?: string | number }>
|
|
2523
|
+
): string {
|
|
2524
|
+
if (!datasets || datasets.length === 0) return "—";
|
|
2525
|
+
return datasets
|
|
2526
|
+
.map((ds) => {
|
|
2527
|
+
const name = ds?.name ?? "(unnamed)";
|
|
2528
|
+
const version = ds?.version != null && ds.version !== "" ? ` (v${ds.version})` : "";
|
|
2529
|
+
return `${name}${version}`;
|
|
2530
|
+
})
|
|
2531
|
+
.join(", ");
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function formatLabels(labels?: string[]): string {
|
|
2535
|
+
if (!labels || labels.length === 0) return "—";
|
|
2536
|
+
return labels.join(", ");
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function formatBoolean(value: unknown): string {
|
|
2540
|
+
if (value === true) return "yes";
|
|
2541
|
+
if (value === false) return "no";
|
|
2542
|
+
return "—";
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function formatNumber(value: unknown): string {
|
|
2546
|
+
if (typeof value !== "number" || Number.isNaN(value)) return "—";
|
|
2547
|
+
return Number.isInteger(value) ? String(value) : value.toString();
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
function formatPercent(value: unknown): string {
|
|
2551
|
+
if (typeof value !== "number" || Number.isNaN(value)) return "—";
|
|
2552
|
+
const pct = value > 1 ? value : value * 100;
|
|
2553
|
+
return `${Number.isInteger(pct) ? pct : pct.toFixed(1)}%`;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
function formatNonEmpty(value: unknown): string {
|
|
2557
|
+
if (typeof value === "string") {
|
|
2558
|
+
const trimmed = value.trim();
|
|
2559
|
+
return trimmed.length ? trimmed : "—";
|
|
2560
|
+
}
|
|
2561
|
+
return "—";
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function jobToDetailRows(job: api.TrainingJob): DetailRow[] {
|
|
2565
|
+
const record = job as unknown as Record<string, unknown>;
|
|
2566
|
+
const provider =
|
|
2567
|
+
typeof record.provider_name === "string" ? record.provider_name : undefined;
|
|
2568
|
+
const progressPercent = typeof record.progress_percent === "number" ? record.progress_percent : undefined;
|
|
2569
|
+
const currentEpoch = typeof record.current_epoch === "number" ? record.current_epoch : undefined;
|
|
2570
|
+
|
|
2571
|
+
const rows: DetailRow[] = [
|
|
2572
|
+
{ label: "ID", value: formatNonEmpty(job.id) },
|
|
2573
|
+
{ label: "Model", value: formatNonEmpty(job.model_name) },
|
|
2574
|
+
{ label: "Base model", value: formatNonEmpty(job.base_model) },
|
|
2575
|
+
{ label: "Task", value: formatNonEmpty(job.task_type) },
|
|
2576
|
+
{ label: "Status", value: formatNonEmpty(job.status), valueColor: statusColor(job.status) },
|
|
2577
|
+
{ label: "Provider", value: formatNonEmpty(provider) },
|
|
2578
|
+
{ label: "Datasets", value: formatDatasetList(job.datasets) },
|
|
2579
|
+
{ label: "Labels", value: formatLabels(job.labels) },
|
|
2580
|
+
{ label: "Epochs", value: formatNumber(job.nr_epochs) },
|
|
2581
|
+
{ label: "Batch size", value: formatNumber(job.batch_size) },
|
|
2582
|
+
{ label: "Learning rate", value: formatNumber(job.learning_rate) },
|
|
2583
|
+
{ label: "Validation split", value: formatPercent(job.validation_data_percentage) },
|
|
2584
|
+
{ label: "Auto-selected", value: formatBoolean(job.model_auto_selected) },
|
|
2585
|
+
{ label: "Version", value: formatNonEmpty(job.version_number) },
|
|
2586
|
+
{ label: "Created", value: formatTimestamp(job.created_at) },
|
|
2587
|
+
{ label: "Started", value: formatTimestamp(job.started_at) },
|
|
2588
|
+
{ label: "Completed", value: formatTimestamp(job.completed_at) },
|
|
2589
|
+
{ label: "Duration", value: formatDuration(job.started_at, job.completed_at) },
|
|
2590
|
+
{ label: "Updated", value: formatTimestamp(job.updated_at) },
|
|
2591
|
+
];
|
|
2592
|
+
|
|
2593
|
+
if (progressPercent !== undefined) {
|
|
2594
|
+
rows.splice(5, 0, {
|
|
2595
|
+
label: "Progress",
|
|
2596
|
+
value: `${progressPercent.toFixed(1)}%${currentEpoch != null ? ` (epoch ${currentEpoch})` : ""}`,
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (job.error_message) {
|
|
2600
|
+
rows.push({
|
|
2601
|
+
label: "Error",
|
|
2602
|
+
value: job.error_message,
|
|
2603
|
+
valueColor: "red",
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
return rows;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
function JobGetCommand({ jobId }: { jobId: string }) {
|
|
2611
|
+
const { exit } = useApp();
|
|
2612
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2613
|
+
const [job, setJob] = useState<api.TrainingJob | null>(null);
|
|
2614
|
+
const [error, setError] = useState("");
|
|
2615
|
+
|
|
2616
|
+
useEffect(() => {
|
|
2617
|
+
let isActive = true;
|
|
2618
|
+
(async () => {
|
|
2619
|
+
const result = await api.getJob(jobId);
|
|
2620
|
+
if (!isActive) return;
|
|
2621
|
+
if (!result.ok) {
|
|
2622
|
+
setError(result.error ?? "Failed to load training job.");
|
|
2623
|
+
setState("error");
|
|
2624
|
+
} else {
|
|
2625
|
+
setJob((result.data as api.TrainingJob | undefined) ?? null);
|
|
2626
|
+
setState("done");
|
|
2627
|
+
}
|
|
2628
|
+
setTimeout(() => exit(), 500);
|
|
2629
|
+
})();
|
|
2630
|
+
return () => {
|
|
2631
|
+
isActive = false;
|
|
2632
|
+
};
|
|
2633
|
+
}, [jobId, exit]);
|
|
2634
|
+
|
|
2635
|
+
if (state === "loading") {
|
|
2636
|
+
return <Loading message={`Loading training job ${jobId}...`} />;
|
|
2637
|
+
}
|
|
2638
|
+
if (state === "error") {
|
|
2639
|
+
return <ErrorMessage error={error} />;
|
|
2640
|
+
}
|
|
2641
|
+
if (!job) {
|
|
2642
|
+
return <ErrorMessage error="Training job response was empty." />;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
return (
|
|
2646
|
+
<DetailView
|
|
2647
|
+
rows={jobToDetailRows(job)}
|
|
2648
|
+
footer={`Use \`pioneer job get ${jobId} --json\` for the raw JSON payload.`}
|
|
2649
|
+
/>
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2654
|
+
// Base Models List Command (tabular)
|
|
2655
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2656
|
+
|
|
2657
|
+
interface BaseModelRow {
|
|
2658
|
+
id: string;
|
|
2659
|
+
name: string;
|
|
2660
|
+
type: string;
|
|
2661
|
+
context: string;
|
|
2662
|
+
inference: string;
|
|
2663
|
+
training: string;
|
|
2664
|
+
[key: string]: string;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
const BASE_MODEL_COLUMNS: TableColumn<BaseModelRow>[] = [
|
|
2668
|
+
{ key: "id", header: "ID", minWidth: 12, maxWidth: 36, flexible: true },
|
|
2669
|
+
{ key: "name", header: "NAME", minWidth: 8, maxWidth: 30, flexible: true },
|
|
2670
|
+
{ key: "type", header: "TYPE", minWidth: 4, maxWidth: 8 },
|
|
2671
|
+
{ key: "context", header: "CONTEXT", minWidth: 7, maxWidth: 8 },
|
|
2672
|
+
{ key: "inference", header: "INFERENCE", minWidth: 9, maxWidth: 9, color: (row) => (row.inference === "yes" ? "green" : "red") },
|
|
2673
|
+
{ key: "training", header: "TRAINING", minWidth: 8, maxWidth: 8, color: (row) => (row.training === "yes" ? "green" : "red") },
|
|
2674
|
+
];
|
|
2675
|
+
|
|
2676
|
+
function formatContextWindow(n?: number): string {
|
|
2677
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) return "—";
|
|
2678
|
+
if (n >= 1_000_000) {
|
|
2679
|
+
const v = n / 1_000_000;
|
|
2680
|
+
return `${Number.isInteger(v) ? v : v.toFixed(1)}M`;
|
|
2681
|
+
}
|
|
2682
|
+
if (n >= 1000) {
|
|
2683
|
+
const v = n / 1000;
|
|
2684
|
+
return `${Number.isInteger(v) ? v : v.toFixed(0)}K`;
|
|
2685
|
+
}
|
|
2686
|
+
return String(n);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function BaseModelsListCommand() {
|
|
2690
|
+
const { exit } = useApp();
|
|
2691
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2692
|
+
const [models, setModels] = useState<api.BaseModelInfo[]>([]);
|
|
2693
|
+
const [error, setError] = useState("");
|
|
2694
|
+
|
|
2695
|
+
useEffect(() => {
|
|
2696
|
+
(async () => {
|
|
2697
|
+
const result = await api.listBaseModels();
|
|
2698
|
+
if (!result.ok) {
|
|
2699
|
+
setError(result.error ?? "Failed to load base models.");
|
|
2700
|
+
setState("error");
|
|
2701
|
+
} else {
|
|
2702
|
+
setModels(normalizeBaseModels(result.data));
|
|
2703
|
+
setState("done");
|
|
2704
|
+
}
|
|
2705
|
+
setTimeout(() => exit(), 500);
|
|
2706
|
+
})();
|
|
2707
|
+
}, [exit]);
|
|
2708
|
+
|
|
2709
|
+
if (state === "loading") return <Loading message="Loading base models..." />;
|
|
2710
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2711
|
+
if (models.length === 0) {
|
|
2712
|
+
return <Text>No base models available.</Text>;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const rows: BaseModelRow[] = models.map((m) => {
|
|
2716
|
+
const inference = m.supports_inference ?? m.supports_on_demand_inference;
|
|
2717
|
+
return {
|
|
2718
|
+
id: m.id || "—",
|
|
2719
|
+
name: (m.label || m.name || m.id || "—").trim() || "—",
|
|
2720
|
+
type: m.task_type || "—",
|
|
2721
|
+
context: formatContextWindow(m.context_window),
|
|
2722
|
+
inference: inference === undefined ? "—" : inference ? "yes" : "no",
|
|
2723
|
+
training: m.supports_training === undefined ? "—" : m.supports_training ? "yes" : "no",
|
|
2724
|
+
};
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
return (
|
|
2728
|
+
<DataTable
|
|
2729
|
+
columns={BASE_MODEL_COLUMNS}
|
|
2730
|
+
rows={rows}
|
|
2731
|
+
footer={`${models.length} base model${models.length === 1 ? "" : "s"} · use \`--json\` for raw JSON`}
|
|
2732
|
+
/>
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2737
|
+
// Model Endpoint Get Command (single-resource key/value layout, with dataset count)
|
|
2738
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2739
|
+
|
|
2740
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2741
|
+
|
|
2742
|
+
function ModelEndpointGetCommand({ modelId }: { modelId: string }) {
|
|
2743
|
+
const { exit } = useApp();
|
|
2744
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2745
|
+
const [project, setProject] = useState<api.ProjectResponse | null>(null);
|
|
2746
|
+
const [datasetCount, setDatasetCount] = useState<api.ProjectDatasetCountResponse | null>(null);
|
|
2747
|
+
const [activeJob, setActiveJob] = useState<api.TrainingJob | null>(null);
|
|
2748
|
+
const [error, setError] = useState("");
|
|
2749
|
+
|
|
2750
|
+
useEffect(() => {
|
|
2751
|
+
let active = true;
|
|
2752
|
+
(async () => {
|
|
2753
|
+
// Sequential to dodge Bun concurrent-fetch keep-alive flakiness.
|
|
2754
|
+
const projectResult = await api.getProject(modelId);
|
|
2755
|
+
if (!active) return;
|
|
2756
|
+
if (!projectResult.ok) {
|
|
2757
|
+
setError(projectResult.error ?? "Failed to load model endpoint.");
|
|
2758
|
+
setState("error");
|
|
2759
|
+
setTimeout(() => exit(), 500);
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
setProject(projectResult.data ?? null);
|
|
2763
|
+
|
|
2764
|
+
const countResult = await api.getProjectDatasetCount(modelId);
|
|
2765
|
+
if (!active) return;
|
|
2766
|
+
if (countResult.ok) setDatasetCount(countResult.data ?? null);
|
|
2767
|
+
|
|
2768
|
+
// If the selected model id is a UUID, it's a training job (a deployed
|
|
2769
|
+
// fine-tuned checkpoint). Hydrate it so we can show a friendly name
|
|
2770
|
+
// instead of just the UUID.
|
|
2771
|
+
const selected = projectResult.data?.selected_model_id ?? "";
|
|
2772
|
+
if (UUID_REGEX.test(selected)) {
|
|
2773
|
+
const jobResult = await api.getJob(selected);
|
|
2774
|
+
if (!active) return;
|
|
2775
|
+
if (jobResult.ok) setActiveJob(jobResult.data ?? null);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
setState("done");
|
|
2779
|
+
setTimeout(() => exit(), 500);
|
|
2780
|
+
})();
|
|
2781
|
+
return () => {
|
|
2782
|
+
active = false;
|
|
2783
|
+
};
|
|
2784
|
+
}, [modelId, exit]);
|
|
2785
|
+
|
|
2786
|
+
if (state === "loading") return <Loading message={`Loading model endpoint ${modelId}...`} />;
|
|
2787
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2788
|
+
if (!project) return <ErrorMessage error="Model endpoint response was empty." />;
|
|
2789
|
+
|
|
2790
|
+
const selected = project.selected_model_id ?? "";
|
|
2791
|
+
const selectedDisplay = (() => {
|
|
2792
|
+
if (!selected) return "—";
|
|
2793
|
+
if (activeJob) {
|
|
2794
|
+
const parts: string[] = [];
|
|
2795
|
+
if (activeJob.model_name) parts.push(activeJob.model_name);
|
|
2796
|
+
if (activeJob.base_model) parts.push(`base: ${activeJob.base_model}`);
|
|
2797
|
+
const tail = parts.length ? ` (${parts.join(", ")})` : "";
|
|
2798
|
+
return `${shortJobId(selected)}${tail}`;
|
|
2799
|
+
}
|
|
2800
|
+
// Stock model id (or UUID we couldn't resolve) — render as-is.
|
|
2801
|
+
return UUID_REGEX.test(selected) ? shortJobId(selected) : selected;
|
|
2802
|
+
})();
|
|
2803
|
+
|
|
2804
|
+
const rows: DetailRow[] = [
|
|
2805
|
+
{ label: "ID", value: formatNonEmpty(project.id) },
|
|
2806
|
+
{ label: "Name", value: formatNonEmpty(project.name) },
|
|
2807
|
+
{ label: "Description", value: formatNonEmpty(project.description) },
|
|
2808
|
+
{ label: "Icon", value: formatNonEmpty(project.icon) },
|
|
2809
|
+
{ label: "Repo", value: formatNonEmpty(project.repo) },
|
|
2810
|
+
{ label: "Selected model", value: selectedDisplay },
|
|
2811
|
+
{ label: "Created", value: formatTimestamp(project.created_at) },
|
|
2812
|
+
{ label: "Updated", value: formatTimestamp(project.updated_at) },
|
|
2813
|
+
];
|
|
2814
|
+
if (datasetCount) {
|
|
2815
|
+
rows.push(
|
|
2816
|
+
{ label: "Datasets", value: formatNumber(datasetCount.dataset_count) },
|
|
2817
|
+
{
|
|
2818
|
+
label: "Can delete",
|
|
2819
|
+
value: formatBoolean(datasetCount.can_delete),
|
|
2820
|
+
valueColor: datasetCount.can_delete ? "green" : "yellow",
|
|
2821
|
+
}
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
return (
|
|
2826
|
+
<DetailView
|
|
2827
|
+
rows={rows}
|
|
2828
|
+
footer={`Use \`pioneer model endpoints get ${modelId} --json\` for the raw JSON payload.`}
|
|
2829
|
+
/>
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2834
|
+
// Model Endpoint Quality Metrics Command (compact key/value layout)
|
|
2835
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2836
|
+
|
|
2837
|
+
function ModelQualityMetricsCommand({ modelId }: { modelId: string }) {
|
|
2838
|
+
const { exit } = useApp();
|
|
2839
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2840
|
+
const [data, setData] = useState<api.QualityMetricsResponse | null>(null);
|
|
2841
|
+
const [error, setError] = useState("");
|
|
2842
|
+
|
|
2843
|
+
useEffect(() => {
|
|
2844
|
+
let active = true;
|
|
2845
|
+
(async () => {
|
|
2846
|
+
const result = await api.getProjectQualityMetrics(modelId);
|
|
2847
|
+
if (!active) return;
|
|
2848
|
+
if (!result.ok) {
|
|
2849
|
+
setError(result.error ?? "Failed to load quality metrics.");
|
|
2850
|
+
setState("error");
|
|
2851
|
+
} else {
|
|
2852
|
+
setData(result.data ?? null);
|
|
2853
|
+
setState("done");
|
|
2854
|
+
}
|
|
2855
|
+
setTimeout(() => exit(), 500);
|
|
2856
|
+
})();
|
|
2857
|
+
return () => {
|
|
2858
|
+
active = false;
|
|
2859
|
+
};
|
|
2860
|
+
}, [modelId, exit]);
|
|
2861
|
+
|
|
2862
|
+
if (state === "loading") return <Loading message={`Loading quality metrics for ${modelId}...`} />;
|
|
2863
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
2864
|
+
if (!data) return <ErrorMessage error="Quality metrics response was empty." />;
|
|
2865
|
+
|
|
2866
|
+
const rows: DetailRow[] = [
|
|
2867
|
+
{ label: "Model ID", value: formatNonEmpty(data.project_id) },
|
|
2868
|
+
{ label: "Total judged", value: formatNumber(data.total_judged) },
|
|
2869
|
+
{ label: "Pass count", value: formatNumber(data.pass_count), valueColor: "green" },
|
|
2870
|
+
{ label: "Fail count", value: formatNumber(data.fail_count), valueColor: data.fail_count > 0 ? "red" : undefined },
|
|
2871
|
+
{ label: "Uncertain count", value: formatNumber(data.uncertain_count), valueColor: data.uncertain_count > 0 ? "yellow" : undefined },
|
|
2872
|
+
{ label: "Pass rate", value: formatPercent(data.pass_rate), valueColor: "green" },
|
|
2873
|
+
{ label: "Fail rate", value: formatPercent(data.fail_rate), valueColor: data.fail_rate > 0 ? "red" : undefined },
|
|
2874
|
+
];
|
|
2875
|
+
|
|
2876
|
+
return (
|
|
2877
|
+
<DetailView
|
|
2878
|
+
rows={rows}
|
|
2879
|
+
footer={`Use \`pioneer model endpoints quality-metrics ${modelId} --json\` for the raw JSON payload.`}
|
|
2880
|
+
/>
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2885
|
+
// Deploy Job Picker (interactive picker for `model endpoints deploy` with no --job)
|
|
2886
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2887
|
+
|
|
2888
|
+
function isJobDeployable(status?: string): boolean {
|
|
2889
|
+
const s = (status ?? "").trim().toLowerCase();
|
|
2890
|
+
return s === "complete" || s === "completed" || s === "succeeded" || s === "deployed";
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/**
|
|
2894
|
+
* Normalize a model identifier to a "family" key for matching.
|
|
2895
|
+
* Strips the org/namespace prefix, lowercases, and drops common
|
|
2896
|
+
* variant suffixes (-instruct, -chat, -base, -it, -hf) so that, e.g.,
|
|
2897
|
+
* "qwen/Qwen3-8B-Instruct" → "qwen3-8b"
|
|
2898
|
+
* "Qwen/Qwen3-8B" → "qwen3-8b"
|
|
2899
|
+
* "meta-llama/Llama-3.1-8B-Instruct" → "llama-3.1-8b"
|
|
2900
|
+
* are treated as the same family.
|
|
2901
|
+
*/
|
|
2902
|
+
function modelFamilyKey(modelId?: string | null): string {
|
|
2903
|
+
if (!modelId) return "";
|
|
2904
|
+
const last = modelId.toLowerCase().split("/").pop() ?? "";
|
|
2905
|
+
return last
|
|
2906
|
+
.replace(/-instruct$/, "")
|
|
2907
|
+
.replace(/-chat$/, "")
|
|
2908
|
+
.replace(/-base$/, "")
|
|
2909
|
+
.replace(/-it$/, "")
|
|
2910
|
+
.replace(/-hf$/, "")
|
|
2911
|
+
.trim();
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
function jobMatchesEndpoint(job: api.TrainingJob, selectedModelId?: string): boolean {
|
|
2915
|
+
if (!selectedModelId) return true;
|
|
2916
|
+
const want = modelFamilyKey(selectedModelId);
|
|
2917
|
+
const have = modelFamilyKey(job.base_model);
|
|
2918
|
+
if (!want || !have) return true;
|
|
2919
|
+
return want === have || want.startsWith(have) || have.startsWith(want);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
export function DeployJobPickerCommand({
|
|
2923
|
+
modelId,
|
|
2924
|
+
reason,
|
|
2925
|
+
showAll,
|
|
2926
|
+
}: {
|
|
2927
|
+
modelId: string;
|
|
2928
|
+
reason?: string;
|
|
2929
|
+
showAll?: boolean;
|
|
2930
|
+
}) {
|
|
2931
|
+
const { exit } = useApp();
|
|
2932
|
+
const { isRawModeSupported } = useStdin();
|
|
2933
|
+
const [phase, setPhase] = useState<"loading" | "picking" | "deploying" | "done" | "error">("loading");
|
|
2934
|
+
const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
|
|
2935
|
+
const [endpointModel, setEndpointModel] = useState<string | undefined>(undefined);
|
|
2936
|
+
const [filterApplied, setFilterApplied] = useState<boolean>(false);
|
|
2937
|
+
const [filterSource, setFilterSource] = useState<"project" | "family" | "none">("none");
|
|
2938
|
+
const [error, setError] = useState("");
|
|
2939
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
2940
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
2941
|
+
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
2942
|
+
const [deployResult, setDeployResult] = useState<unknown>(null);
|
|
2943
|
+
|
|
2944
|
+
useEffect(() => {
|
|
2945
|
+
let active = true;
|
|
2946
|
+
(async () => {
|
|
2947
|
+
// Run sequentially to avoid Bun concurrent-fetch keep-alive flakiness
|
|
2948
|
+
// ("socket connection was closed unexpectedly"). Retry once on transient
|
|
2949
|
+
// network errors.
|
|
2950
|
+
const isTransient = (err?: string) =>
|
|
2951
|
+
!!err && /socket|ECONNRESET|fetch failed|network|EAI_AGAIN|timeout/i.test(err);
|
|
2952
|
+
|
|
2953
|
+
const fetchWithRetry = async <T,>(fn: () => Promise<api.ApiResult<T>>): Promise<api.ApiResult<T>> => {
|
|
2954
|
+
let r = await fn();
|
|
2955
|
+
if (!r.ok && (r.status === 0 || isTransient(r.error))) {
|
|
2956
|
+
await new Promise((res) => setTimeout(res, 250));
|
|
2957
|
+
r = await fn();
|
|
2958
|
+
}
|
|
2959
|
+
return r;
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
const sortByRecent = (list: api.TrainingJob[]) =>
|
|
2963
|
+
list.slice().sort((a, b) => {
|
|
2964
|
+
const at = Date.parse(a.completed_at ?? a.updated_at ?? a.created_at ?? "");
|
|
2965
|
+
const bt = Date.parse(b.completed_at ?? b.updated_at ?? b.created_at ?? "");
|
|
2966
|
+
if (Number.isNaN(at) || Number.isNaN(bt)) return 0;
|
|
2967
|
+
return bt - at;
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
// 1. Resolve the endpoint. A hard 404/403 means the endpoint doesn't exist
|
|
2971
|
+
// on this backend — fail fast rather than open a picker for a target
|
|
2972
|
+
// that any subsequent deploy call will reject. Other errors (5xx,
|
|
2973
|
+
// transient network) degrade gracefully: we just can't apply the
|
|
2974
|
+
// family filter.
|
|
2975
|
+
const projectResult = await fetchWithRetry(() => api.getProject(modelId));
|
|
2976
|
+
if (!active) return;
|
|
2977
|
+
if (!projectResult.ok && (projectResult.status === 404 || projectResult.status === 403)) {
|
|
2978
|
+
const reason =
|
|
2979
|
+
projectResult.status === 403
|
|
2980
|
+
? "You do not have access to this endpoint."
|
|
2981
|
+
: "This endpoint does not exist on the current backend.";
|
|
2982
|
+
setError(
|
|
2983
|
+
`${reason} (model id: ${modelId})\nList valid endpoints with: pioneer model endpoints list`
|
|
2984
|
+
);
|
|
2985
|
+
setPhase("error");
|
|
2986
|
+
setTimeout(() => exit(), 500);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const selected = projectResult.ok ? projectResult.data?.selected_model_id : undefined;
|
|
2990
|
+
setEndpointModel(selected ?? undefined);
|
|
2991
|
+
|
|
2992
|
+
// 2. Try server-side filter by project_id. Today most jobs have project_id=null
|
|
2993
|
+
// so this often returns []; we fall back to all-jobs + family match below.
|
|
2994
|
+
let list: api.TrainingJob[] = [];
|
|
2995
|
+
let didFilter = false;
|
|
2996
|
+
let filterSource: "project" | "family" | "none" = "none";
|
|
2997
|
+
|
|
2998
|
+
if (!showAll) {
|
|
2999
|
+
const scoped = await fetchWithRetry(() => api.listJobs({ project_id: modelId }));
|
|
3000
|
+
if (!active) return;
|
|
3001
|
+
if (scoped.ok) {
|
|
3002
|
+
const scopedDeployable = sortByRecent(
|
|
3003
|
+
(scoped.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
|
|
3004
|
+
);
|
|
3005
|
+
if (scopedDeployable.length > 0) {
|
|
3006
|
+
list = scopedDeployable;
|
|
3007
|
+
didFilter = true;
|
|
3008
|
+
filterSource = "project";
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// 3. Fall back to all jobs (and optionally family-match against the endpoint).
|
|
3014
|
+
if (list.length === 0) {
|
|
3015
|
+
const allResult = await fetchWithRetry(() => api.listJobs());
|
|
3016
|
+
if (!active) return;
|
|
3017
|
+
if (!allResult.ok) {
|
|
3018
|
+
setError(allResult.error ?? "Failed to load training jobs.");
|
|
3019
|
+
setPhase("error");
|
|
3020
|
+
setTimeout(() => exit(), 500);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const allDeployable = sortByRecent(
|
|
3024
|
+
(allResult.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
|
|
3025
|
+
);
|
|
3026
|
+
|
|
3027
|
+
if (!showAll && selected) {
|
|
3028
|
+
const familyMatched = allDeployable.filter((j) => jobMatchesEndpoint(j, selected));
|
|
3029
|
+
if (familyMatched.length > 0) {
|
|
3030
|
+
list = familyMatched;
|
|
3031
|
+
didFilter = true;
|
|
3032
|
+
filterSource = "family";
|
|
3033
|
+
} else {
|
|
3034
|
+
list = allDeployable;
|
|
3035
|
+
}
|
|
3036
|
+
} else {
|
|
3037
|
+
list = allDeployable;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
setFilterApplied(didFilter);
|
|
3042
|
+
setFilterSource(filterSource);
|
|
3043
|
+
setJobs(list);
|
|
3044
|
+
setPhase("picking");
|
|
3045
|
+
})();
|
|
3046
|
+
return () => {
|
|
3047
|
+
active = false;
|
|
3048
|
+
};
|
|
3049
|
+
}, [exit, modelId, showAll]);
|
|
3050
|
+
|
|
3051
|
+
const matching = jobs.filter((job) => {
|
|
3052
|
+
const q = searchQuery.trim().toLowerCase();
|
|
3053
|
+
if (!q) return true;
|
|
3054
|
+
return (
|
|
3055
|
+
(job.id ?? "").toLowerCase().includes(q) ||
|
|
3056
|
+
(job.model_name ?? "").toLowerCase().includes(q) ||
|
|
3057
|
+
(job.base_model ?? "").toLowerCase().includes(q)
|
|
3058
|
+
);
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
const deploy = async (jobId: string) => {
|
|
3062
|
+
setSelectedJobId(jobId);
|
|
3063
|
+
setPhase("deploying");
|
|
3064
|
+
|
|
3065
|
+
// The deploy endpoint requires `training_jobs.project_id == project_id`
|
|
3066
|
+
// (server-side WHERE clause). Most jobs are created with project_id=null,
|
|
3067
|
+
// so we PATCH first to link the job to the target project. The PATCH is
|
|
3068
|
+
// a no-op if the job is already linked.
|
|
3069
|
+
const job = jobs.find((j) => j.id === jobId);
|
|
3070
|
+
if (!job || job.project_id !== modelId) {
|
|
3071
|
+
const patch = await api.updateTrainingJob(jobId, { project_id: modelId });
|
|
3072
|
+
if (!patch.ok) {
|
|
3073
|
+
const detail =
|
|
3074
|
+
patch.status === 403
|
|
3075
|
+
? "You do not have permission to assign this job to this project."
|
|
3076
|
+
: patch.error ?? "Failed to link job to project.";
|
|
3077
|
+
setError(`Could not assign job to project before deploy: ${detail}`);
|
|
3078
|
+
setPhase("error");
|
|
3079
|
+
setTimeout(() => exit(), 1500);
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const result = await api.deployTrainingJobToProject(modelId, {
|
|
3085
|
+
training_job_id: jobId,
|
|
3086
|
+
...(reason ? { reason } : {}),
|
|
3087
|
+
});
|
|
3088
|
+
if (!result.ok) {
|
|
3089
|
+
let msg = result.error ?? "Deployment failed.";
|
|
3090
|
+
if (result.status === 404) {
|
|
3091
|
+
msg =
|
|
3092
|
+
`Backend rejected the deploy with 404 "${result.error ?? "Resource not found."}".\n` +
|
|
3093
|
+
` • Project ${modelId} and training job ${jobId} both exist and are linked.\n` +
|
|
3094
|
+
` • Inspect job state: pioneer job get ${jobId}`;
|
|
3095
|
+
}
|
|
3096
|
+
setError(msg);
|
|
3097
|
+
setPhase("error");
|
|
3098
|
+
} else {
|
|
3099
|
+
// CLI-side enrichment: the deploy record's `base_model` is null for
|
|
3100
|
+
// training-job deploys (mutually-exclusive with `training_job_id` in
|
|
3101
|
+
// the current schema), but users want to see what base the fine-tune
|
|
3102
|
+
// is built on. Populate it from the picked job until the backend
|
|
3103
|
+
// change lands.
|
|
3104
|
+
const enriched =
|
|
3105
|
+
result.data && typeof result.data === "object" && job?.base_model
|
|
3106
|
+
? { ...result.data, base_model: (result.data as { base_model?: string | null }).base_model ?? job.base_model }
|
|
3107
|
+
: result.data;
|
|
3108
|
+
setDeployResult(enriched);
|
|
3109
|
+
setPhase("done");
|
|
3110
|
+
}
|
|
3111
|
+
setTimeout(() => exit(), 1500);
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
useInput(
|
|
3115
|
+
(input, key) => {
|
|
3116
|
+
if (phase !== "picking" || !matching.length) {
|
|
3117
|
+
if (key.return && phase === "picking" && !matching.length) {
|
|
3118
|
+
setError("No matching jobs.");
|
|
3119
|
+
}
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (key.upArrow) {
|
|
3123
|
+
setHighlightIndex((idx) => (idx === 0 ? matching.length - 1 : idx - 1));
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (key.downArrow) {
|
|
3127
|
+
setHighlightIndex((idx) => (idx === matching.length - 1 ? 0 : idx + 1));
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
if (key.return) {
|
|
3131
|
+
const selected = matching[highlightIndex];
|
|
3132
|
+
if (selected?.id) {
|
|
3133
|
+
void deploy(selected.id);
|
|
3134
|
+
}
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (key.backspace || key.delete) {
|
|
3138
|
+
setSearchQuery((q) => q.slice(0, -1));
|
|
3139
|
+
setHighlightIndex(0);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
if (input && !key.ctrl && !key.meta) {
|
|
3143
|
+
setSearchQuery((q) => q + input);
|
|
3144
|
+
setHighlightIndex(0);
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
{ isActive: isRawModeSupported && phase === "picking" }
|
|
3148
|
+
);
|
|
3149
|
+
|
|
3150
|
+
if (!isRawModeSupported) {
|
|
3151
|
+
return (
|
|
3152
|
+
<Box flexDirection="column">
|
|
3153
|
+
<ErrorMessage error="Interactive job picker requires a TTY." />
|
|
3154
|
+
<Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
|
|
3155
|
+
<Text dimColor> pioneer model endpoints deploy {modelId} --job <training-job-id></Text>
|
|
3156
|
+
</Box>
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
1374
3159
|
|
|
1375
|
-
if (
|
|
3160
|
+
if (phase === "loading") return <Loading message="Loading deployable training jobs..." />;
|
|
3161
|
+
if (phase === "error") return <ErrorMessage error={error} />;
|
|
3162
|
+
|
|
3163
|
+
if (phase === "deploying") {
|
|
3164
|
+
return <Loading message={`Deploying job ${selectedJobId} to endpoint ${modelId}...`} />;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
if (phase === "done") {
|
|
1376
3168
|
return (
|
|
1377
3169
|
<Box flexDirection="column">
|
|
1378
|
-
<
|
|
1379
|
-
|
|
1380
|
-
|
|
3170
|
+
<Success message={`Deployment initiated for endpoint ${modelId} from job ${selectedJobId}`} />
|
|
3171
|
+
{deployResult ? (
|
|
3172
|
+
<Text dimColor>{JSON.stringify(deployResult, null, 2)}</Text>
|
|
3173
|
+
) : null}
|
|
1381
3174
|
</Box>
|
|
1382
3175
|
);
|
|
1383
3176
|
}
|
|
1384
3177
|
|
|
1385
|
-
if (
|
|
3178
|
+
if (jobs.length === 0) {
|
|
1386
3179
|
return (
|
|
1387
|
-
<
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
...(repo ? { repo } : {}),
|
|
1393
|
-
...(description ? { description } : {}),
|
|
1394
|
-
active_model_id: selectedModelId,
|
|
1395
|
-
selected_model_id: selectedModelId,
|
|
1396
|
-
...(example ? { example } : {}),
|
|
1397
|
-
})
|
|
1398
|
-
}
|
|
1399
|
-
successMessage="Model entry created"
|
|
1400
|
-
/>
|
|
3180
|
+
<Box flexDirection="column">
|
|
3181
|
+
<ErrorMessage error="No deployable training jobs found." />
|
|
3182
|
+
<Text dimColor>Only jobs with status `complete` / `succeeded` / `deployed` are eligible.</Text>
|
|
3183
|
+
<Text dimColor>Run `pioneer job list` to inspect job statuses, or start training with `pioneer agent`.</Text>
|
|
3184
|
+
</Box>
|
|
1401
3185
|
);
|
|
1402
3186
|
}
|
|
1403
3187
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
3188
|
+
const visible = matching.slice(0, 12);
|
|
3189
|
+
const offsetIndex = Math.max(0, Math.min(highlightIndex - visible.length + 1, matching.length - visible.length));
|
|
3190
|
+
const window = matching.slice(offsetIndex, offsetIndex + visible.length);
|
|
3191
|
+
|
|
3192
|
+
const navHint = "Type to filter · ↑/↓ navigate · Enter to deploy";
|
|
3193
|
+
const headerLine = (() => {
|
|
3194
|
+
const n = jobs.length;
|
|
3195
|
+
const noun = `job${n === 1 ? "" : "s"}`;
|
|
3196
|
+
if (showAll) {
|
|
3197
|
+
const tail = endpointModel ? ` Endpoint base model: "${endpointModel}".` : "";
|
|
3198
|
+
return `Showing all ${n} deployable ${noun} (--all).${tail} ${navHint}.`;
|
|
3199
|
+
}
|
|
3200
|
+
if (filterApplied && filterSource === "project") {
|
|
3201
|
+
return `Showing ${n} ${noun} scoped to this endpoint via project_id. ${navHint} · pass --all to see every job.`;
|
|
3202
|
+
}
|
|
3203
|
+
if (filterApplied && filterSource === "family" && endpointModel) {
|
|
3204
|
+
return `Showing ${n} ${noun} matching base model "${endpointModel}". ${navHint} · pass --all to disable family match.`;
|
|
3205
|
+
}
|
|
3206
|
+
if (endpointModel) {
|
|
3207
|
+
return `No jobs scoped to this endpoint and none match base model "${endpointModel}". Showing all ${n} deployable ${noun} as fallback. ${navHint}.`;
|
|
3208
|
+
}
|
|
3209
|
+
return `Showing ${n} deployable ${noun}. ${navHint}.`;
|
|
3210
|
+
})();
|
|
1407
3211
|
|
|
1408
3212
|
return (
|
|
1409
3213
|
<Box flexDirection="column">
|
|
1410
|
-
<Text bold>
|
|
1411
|
-
<Text>
|
|
1412
|
-
<Text dimColor>Type any part of model id, name, label, or description.</Text>
|
|
1413
|
-
<Text> </Text>
|
|
1414
|
-
<TextInput
|
|
1415
|
-
value={query}
|
|
1416
|
-
onChange={(value) => setQuery(value)}
|
|
1417
|
-
onSubmit={(value) => {
|
|
1418
|
-
const resolvedModel = resolveModelId(value);
|
|
1419
|
-
if (!resolvedModel) {
|
|
1420
|
-
setError("No matching models found. Refine your search and try again.");
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
setSelectedModelId(resolvedModel);
|
|
1424
|
-
}}
|
|
1425
|
-
placeholder="e.g. qwen/qwen3-8b"
|
|
1426
|
-
/>
|
|
3214
|
+
<Text bold>Pick a training job to deploy to endpoint <Text color="cyan">{modelId}</Text>:</Text>
|
|
3215
|
+
<Text dimColor>{headerLine}</Text>
|
|
1427
3216
|
<Text> </Text>
|
|
1428
|
-
{
|
|
1429
|
-
<Text
|
|
3217
|
+
{searchQuery ? (
|
|
3218
|
+
<Text>
|
|
3219
|
+
<Text dimColor>Filter: </Text>
|
|
3220
|
+
<Text>{searchQuery}</Text>
|
|
3221
|
+
</Text>
|
|
3222
|
+
) : null}
|
|
3223
|
+
{window.length === 0 ? (
|
|
3224
|
+
<Text color="yellow">No jobs match `{searchQuery}`.</Text>
|
|
1430
3225
|
) : (
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
3226
|
+
window.map((job, idx) => {
|
|
3227
|
+
const realIndex = offsetIndex + idx;
|
|
3228
|
+
const isHighlighted = realIndex === highlightIndex;
|
|
3229
|
+
const idShort = shortJobId(job.id);
|
|
3230
|
+
const modelName = job.model_name?.trim() || "—";
|
|
3231
|
+
const baseModel = shortBaseModel(job.base_model);
|
|
3232
|
+
const status = job.status ?? "—";
|
|
3233
|
+
const completed = relativeTime(job.completed_at ?? job.updated_at ?? job.created_at);
|
|
3234
|
+
return (
|
|
3235
|
+
<Text key={job.id || realIndex} color={isHighlighted ? "cyan" : undefined} bold={isHighlighted}>
|
|
3236
|
+
{isHighlighted ? "▶ " : " "}
|
|
3237
|
+
{padCell(idShort, 8)} {padCell(modelName, 28)} {padCell(baseModel, 22)} <Text color={statusColor(status)}>{padCell(status, 10)}</Text> {completed}
|
|
3238
|
+
</Text>
|
|
3239
|
+
);
|
|
3240
|
+
})
|
|
1444
3241
|
)}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
3242
|
+
{error ? (
|
|
3243
|
+
<Box marginTop={1}>
|
|
3244
|
+
<Text color="red">{error}</Text>
|
|
3245
|
+
</Box>
|
|
3246
|
+
) : null}
|
|
1449
3247
|
</Box>
|
|
1450
3248
|
);
|
|
1451
3249
|
}
|
|
1452
3250
|
|
|
3251
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3252
|
+
// Model Artifact Download Command (key/value layout for a download URL response)
|
|
3253
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3254
|
+
|
|
3255
|
+
function ModelArtifactDownloadCommand({ jobId }: { jobId: string }) {
|
|
3256
|
+
const { exit } = useApp();
|
|
3257
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
3258
|
+
const [data, setData] = useState<Record<string, unknown> | null>(null);
|
|
3259
|
+
const [error, setError] = useState("");
|
|
3260
|
+
|
|
3261
|
+
useEffect(() => {
|
|
3262
|
+
let active = true;
|
|
3263
|
+
(async () => {
|
|
3264
|
+
const result = await api.downloadModel(jobId);
|
|
3265
|
+
if (!active) return;
|
|
3266
|
+
if (!result.ok) {
|
|
3267
|
+
setError(result.error ?? "Failed to fetch model artifact download URL.");
|
|
3268
|
+
setState("error");
|
|
3269
|
+
} else {
|
|
3270
|
+
setData((result.data as Record<string, unknown> | undefined) ?? null);
|
|
3271
|
+
setState("done");
|
|
3272
|
+
}
|
|
3273
|
+
setTimeout(() => exit(), 500);
|
|
3274
|
+
})();
|
|
3275
|
+
return () => {
|
|
3276
|
+
active = false;
|
|
3277
|
+
};
|
|
3278
|
+
}, [jobId, exit]);
|
|
3279
|
+
|
|
3280
|
+
if (state === "loading") return <Loading message={`Fetching download URL for ${jobId}...`} />;
|
|
3281
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
3282
|
+
if (!data) return <ErrorMessage error="Download response was empty." />;
|
|
3283
|
+
|
|
3284
|
+
const stringField = (key: string) => {
|
|
3285
|
+
const v = data[key];
|
|
3286
|
+
return typeof v === "string" && v.trim() ? v : undefined;
|
|
3287
|
+
};
|
|
3288
|
+
const numberField = (key: string) => {
|
|
3289
|
+
const v = data[key];
|
|
3290
|
+
return typeof v === "number" ? v : undefined;
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
const downloadUrl = stringField("download_url") || stringField("url");
|
|
3294
|
+
const expiresAt = stringField("expires_at") || stringField("expiry");
|
|
3295
|
+
const sizeBytes = numberField("size") ?? numberField("size_bytes");
|
|
3296
|
+
const filename = stringField("filename") || stringField("file_name");
|
|
3297
|
+
const contentType = stringField("content_type") || stringField("mime_type");
|
|
3298
|
+
|
|
3299
|
+
const rows: DetailRow[] = [
|
|
3300
|
+
{ label: "Job ID", value: jobId },
|
|
3301
|
+
{ label: "Filename", value: formatNonEmpty(filename) },
|
|
3302
|
+
{ label: "Content type", value: formatNonEmpty(contentType) },
|
|
3303
|
+
{ label: "Size", value: sizeBytes !== undefined ? `${sizeBytes.toLocaleString()} bytes` : "—" },
|
|
3304
|
+
{ label: "Expires", value: expiresAt ? formatTimestamp(expiresAt) : "—" },
|
|
3305
|
+
{ label: "Download URL", value: downloadUrl || "—", valueColor: downloadUrl ? "cyan" : undefined },
|
|
3306
|
+
];
|
|
3307
|
+
|
|
3308
|
+
return (
|
|
3309
|
+
<DetailView
|
|
3310
|
+
rows={rows}
|
|
3311
|
+
footer={
|
|
3312
|
+
downloadUrl
|
|
3313
|
+
? `Tip: pipe the URL into curl, e.g. \`curl -L -o artifact.zip "$URL"\` · use \`--json\` for raw JSON`
|
|
3314
|
+
: `Use \`pioneer model artifacts download ${jobId} --json\` for the raw JSON payload.`
|
|
3315
|
+
}
|
|
3316
|
+
/>
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
1453
3320
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1454
3321
|
// Job Logs Command (prettified output)
|
|
1455
3322
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1651,462 +3518,195 @@ function GenerateCommand<T extends GenerateResult>({
|
|
|
1651
3518
|
<Text color="cyan">Saved to: {savedPath}</Text>
|
|
1652
3519
|
)}
|
|
1653
3520
|
</Box>
|
|
1654
|
-
);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1658
|
-
// Helper: Infer format from file extension
|
|
1659
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1660
|
-
|
|
1661
|
-
function inferFormatFromPath(
|
|
1662
|
-
path: string | undefined,
|
|
1663
|
-
defaultFormat: string = "jsonl"
|
|
1664
|
-
): "csv" | "jsonl" | "parquet" {
|
|
1665
|
-
if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
1666
|
-
|
|
1667
|
-
const ext = path.toLowerCase().split(".").pop();
|
|
1668
|
-
if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
|
|
1669
|
-
return ext;
|
|
1670
|
-
}
|
|
1671
|
-
return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1675
|
-
// Dataset List Command (shows both remote and local)
|
|
1676
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1677
|
-
|
|
1678
|
-
function DatasetListCommand() {
|
|
1679
|
-
const { exit } = useApp();
|
|
1680
|
-
const [state, setState] = useState<"loading" | "done">("loading");
|
|
1681
|
-
const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
|
|
1682
|
-
const [remoteError, setRemoteError] = useState<string | null>(null);
|
|
1683
|
-
const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
|
|
1684
|
-
|
|
1685
|
-
useEffect(() => {
|
|
1686
|
-
(async () => {
|
|
1687
|
-
// Fetch remote and local datasets
|
|
1688
|
-
const result = await api.listDatasets();
|
|
1689
|
-
const local = listLocalDatasets();
|
|
1690
|
-
setLocalDatasets(local);
|
|
1691
|
-
|
|
1692
|
-
if (result.ok) {
|
|
1693
|
-
setRemoteData(result.data ?? null);
|
|
1694
|
-
} else {
|
|
1695
|
-
setRemoteError(result.error ?? "Unknown error");
|
|
1696
|
-
}
|
|
1697
|
-
setState("done");
|
|
1698
|
-
setTimeout(() => exit(), 500);
|
|
1699
|
-
})();
|
|
1700
|
-
}, [exit]);
|
|
1701
|
-
|
|
1702
|
-
if (state === "loading") {
|
|
1703
|
-
return <Loading />;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
const remoteDatasets = remoteData?.datasets ?? [];
|
|
1707
|
-
|
|
1708
|
-
return (
|
|
1709
|
-
<Box flexDirection="column">
|
|
1710
|
-
<Text bold color="cyan">Remote Datasets {remoteError ? "" : `(${remoteDatasets.length})`}</Text>
|
|
1711
|
-
{remoteError ? (
|
|
1712
|
-
<Box flexDirection="column">
|
|
1713
|
-
{remoteError.split("\n").map((line, idx) => (
|
|
1714
|
-
<Text key={idx} color="red"> {line}</Text>
|
|
1715
|
-
))}
|
|
1716
|
-
</Box>
|
|
1717
|
-
) : remoteDatasets.length === 0 ? (
|
|
1718
|
-
<Text dimColor> No remote datasets</Text>
|
|
1719
|
-
) : (
|
|
1720
|
-
remoteDatasets.map((ds) => (
|
|
1721
|
-
<Box key={ds.id} flexDirection="column">
|
|
1722
|
-
<Text>
|
|
1723
|
-
{" "}<Text color="yellow">{ds.dataset_name}:{ds.version_number || "v1"}</Text> <Text dimColor>({ds.dataset_type}, {ds.sample_size} examples)</Text>
|
|
1724
|
-
</Text>
|
|
1725
|
-
<Text dimColor> {ds.id}</Text>
|
|
1726
|
-
</Box>
|
|
1727
|
-
))
|
|
1728
|
-
)}
|
|
1729
|
-
<Text> </Text>
|
|
1730
|
-
<Text bold color="cyan">Local Datasets ({localDatasets.length})</Text>
|
|
1731
|
-
{localDatasets.length === 0 ? (
|
|
1732
|
-
<Text dimColor> No local datasets in ./datasets/</Text>
|
|
1733
|
-
) : (
|
|
1734
|
-
localDatasets.map((ds) => (
|
|
1735
|
-
<Text key={ds.id}>
|
|
1736
|
-
{" "}<Text color="green">{ds.name}</Text> <Text dimColor>({ds.type}, {ds.sample_size} examples)</Text>
|
|
1737
|
-
</Text>
|
|
1738
|
-
))
|
|
1739
|
-
)}
|
|
1740
|
-
</Box>
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1745
|
-
// Job Create Command (with auto-upload for local datasets)
|
|
1746
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1747
|
-
|
|
1748
|
-
interface ParsedDataset {
|
|
1749
|
-
type: "local" | "remote";
|
|
1750
|
-
name?: string; // for remote name:version format
|
|
1751
|
-
version?: string; // for remote name:version format
|
|
1752
|
-
id?: string; // for remote UUID format
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
interface JobCreateCommandProps {
|
|
1756
|
-
modelName: string;
|
|
1757
|
-
datasets: ParsedDataset[];
|
|
1758
|
-
baseModel?: string;
|
|
1759
|
-
epochs?: number;
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
interface DatasetUploadStatus {
|
|
1763
|
-
name: string;
|
|
1764
|
-
path: string;
|
|
1765
|
-
status: "pending" | "uploading" | "done" | "error";
|
|
1766
|
-
error?: string;
|
|
1767
|
-
uploadedRef?: api.DatasetRef;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// UUID pattern: 8-4-4-4-12 hex characters
|
|
1771
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1772
|
-
|
|
1773
|
-
function isUUID(str: string): boolean {
|
|
1774
|
-
return UUID_PATTERN.test(str);
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function parseDatasetString(ds: string): ParsedDataset | { error: string } {
|
|
1778
|
-
const trimmed = ds.trim();
|
|
1779
|
-
|
|
1780
|
-
if (trimmed.startsWith("local:")) {
|
|
1781
|
-
const name = trimmed.slice(6); // remove "local:"
|
|
1782
|
-
if (!name) {
|
|
1783
|
-
return { error: `Invalid local dataset format: ${ds}. Use local:<name>` };
|
|
1784
|
-
}
|
|
1785
|
-
return { type: "local", name };
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
if (trimmed.startsWith("remote:")) {
|
|
1789
|
-
const rest = trimmed.slice(7); // remove "remote:"
|
|
1790
|
-
|
|
1791
|
-
// Check if it's a UUID
|
|
1792
|
-
if (isUUID(rest)) {
|
|
1793
|
-
return { type: "remote", id: rest };
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
// Otherwise parse as name:version
|
|
1797
|
-
const colonIndex = rest.lastIndexOf(":");
|
|
1798
|
-
if (colonIndex === -1) {
|
|
1799
|
-
// No version, default to latest
|
|
1800
|
-
return { type: "remote", name: rest, version: "latest" };
|
|
1801
|
-
}
|
|
1802
|
-
return {
|
|
1803
|
-
type: "remote",
|
|
1804
|
-
name: rest.slice(0, colonIndex),
|
|
1805
|
-
version: rest.slice(colonIndex + 1),
|
|
1806
|
-
};
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
return { error: `Invalid dataset format: ${ds}. Use remote:<name>:<version>, remote:<uuid>, or local:<name>` };
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
function resolveLocalDatasetPath(name: string): string | null {
|
|
1813
|
-
// Check if it's a local dataset by name (in ./datasets/ directory)
|
|
1814
|
-
const localPath = path.join(DATASETS_DIR, `${name}.json`);
|
|
1815
|
-
if (fs.existsSync(localPath)) {
|
|
1816
|
-
return localPath;
|
|
1817
|
-
}
|
|
1818
|
-
// Check if it's a direct file path
|
|
1819
|
-
if (fs.existsSync(name) && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
|
|
1820
|
-
return name;
|
|
1821
|
-
}
|
|
1822
|
-
return null;
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function getLocalDatasetType(filePath: string): "ner" | "classification" | "custom" {
|
|
1826
|
-
try {
|
|
1827
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1828
|
-
const parsed = JSON.parse(content);
|
|
1829
|
-
const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
|
|
1830
|
-
if (parsed.task_type) {
|
|
1831
|
-
return parsed.task_type as "ner" | "classification" | "custom";
|
|
1832
|
-
}
|
|
1833
|
-
const firstItem = data[0] as Record<string, unknown> | undefined;
|
|
1834
|
-
if (firstItem?.spans) return "ner";
|
|
1835
|
-
if (firstItem?.label) return "classification";
|
|
1836
|
-
return "custom";
|
|
1837
|
-
} catch {
|
|
1838
|
-
return "custom";
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
function convertJsonToJsonl(filePath: string): string {
|
|
1843
|
-
// Convert JSON array to JSONL format for upload
|
|
1844
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
1845
|
-
const parsed = JSON.parse(content);
|
|
1846
|
-
const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
|
|
1847
|
-
|
|
1848
|
-
// Create temp JSONL file
|
|
1849
|
-
const tempPath = filePath.replace(".json", ".jsonl");
|
|
1850
|
-
const jsonlContent = data.map((item: unknown) => JSON.stringify(item)).join("\n");
|
|
1851
|
-
fs.writeFileSync(tempPath, jsonlContent);
|
|
1852
|
-
return tempPath;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
function JobCreateCommand({ modelName, datasets, baseModel, epochs }: JobCreateCommandProps) {
|
|
1856
|
-
const { exit } = useApp();
|
|
1857
|
-
const [state, setState] = useState<"resolving" | "uploading" | "creating" | "done" | "error">("resolving");
|
|
1858
|
-
const [uploadStatuses, setUploadStatuses] = useState<DatasetUploadStatus[]>([]);
|
|
1859
|
-
const [jobResult, setJobResult] = useState<api.TrainingJob | null>(null);
|
|
1860
|
-
const [error, setError] = useState("");
|
|
1861
|
-
|
|
1862
|
-
useEffect(() => {
|
|
1863
|
-
(async () => {
|
|
1864
|
-
// Separate local and remote datasets
|
|
1865
|
-
const localDatasets = datasets.filter((d): d is ParsedDataset & { type: "local"; name: string } =>
|
|
1866
|
-
d.type === "local" && !!d.name
|
|
1867
|
-
);
|
|
1868
|
-
const remoteByNameVersion = datasets.filter((d): d is ParsedDataset & { type: "remote"; name: string } =>
|
|
1869
|
-
d.type === "remote" && !!d.name
|
|
1870
|
-
);
|
|
1871
|
-
const remoteByUUID = datasets.filter((d): d is ParsedDataset & { type: "remote"; id: string } =>
|
|
1872
|
-
d.type === "remote" && !!d.id
|
|
1873
|
-
);
|
|
1874
|
-
|
|
1875
|
-
// Resolve UUIDs to name:version
|
|
1876
|
-
const remoteDatasets: api.DatasetRef[] = remoteByNameVersion.map(d => ({
|
|
1877
|
-
name: d.name,
|
|
1878
|
-
version: d.version ?? "latest"
|
|
1879
|
-
}));
|
|
1880
|
-
|
|
1881
|
-
if (remoteByUUID.length > 0) {
|
|
1882
|
-
// Fetch dataset list to resolve UUIDs
|
|
1883
|
-
const listResult = await api.listDatasets();
|
|
1884
|
-
if (!listResult.ok || !listResult.data) {
|
|
1885
|
-
setError(`Failed to resolve dataset UUIDs: ${listResult.error ?? "Unknown error"}`);
|
|
1886
|
-
setState("error");
|
|
1887
|
-
setTimeout(() => exit(), 500);
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
for (const uuidDataset of remoteByUUID) {
|
|
1892
|
-
const found = listResult.data.datasets.find(ds => ds.id === uuidDataset.id);
|
|
1893
|
-
if (!found) {
|
|
1894
|
-
setError(`Dataset not found with UUID: ${uuidDataset.id}`);
|
|
1895
|
-
setState("error");
|
|
1896
|
-
setTimeout(() => exit(), 500);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
remoteDatasets.push({
|
|
1900
|
-
name: found.dataset_name,
|
|
1901
|
-
version: found.version_number ?? "latest",
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
// Validate local datasets exist
|
|
1907
|
-
const statuses: DatasetUploadStatus[] = [];
|
|
1908
|
-
for (const local of localDatasets) {
|
|
1909
|
-
const localPath = resolveLocalDatasetPath(local.name);
|
|
1910
|
-
if (!localPath) {
|
|
1911
|
-
setError(`Local dataset not found: ${local.name}`);
|
|
1912
|
-
setState("error");
|
|
1913
|
-
setTimeout(() => exit(), 500);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
statuses.push({ name: local.name, path: localPath, status: "pending" });
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
if (statuses.length === 0) {
|
|
1920
|
-
// No local datasets, proceed directly to job creation
|
|
1921
|
-
setState("creating");
|
|
1922
|
-
const result = await api.createJob({
|
|
1923
|
-
model_name: modelName,
|
|
1924
|
-
datasets: remoteDatasets,
|
|
1925
|
-
base_model: baseModel,
|
|
1926
|
-
nr_epochs: epochs,
|
|
1927
|
-
});
|
|
1928
|
-
if (result.ok && result.data) {
|
|
1929
|
-
setJobResult(result.data);
|
|
1930
|
-
setState("done");
|
|
1931
|
-
} else {
|
|
1932
|
-
setError(result.error ?? "Failed to create job");
|
|
1933
|
-
setState("error");
|
|
1934
|
-
}
|
|
1935
|
-
setTimeout(() => exit(), 500);
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// Upload local datasets
|
|
1940
|
-
setUploadStatuses(statuses);
|
|
1941
|
-
setState("uploading");
|
|
1942
|
-
|
|
1943
|
-
const uploadedDatasets: api.DatasetRef[] = [...remoteDatasets];
|
|
1944
|
-
let hasError = false;
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
1945
3523
|
|
|
1946
|
-
|
|
1947
|
-
|
|
3524
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3525
|
+
// Helper: Infer format from file extension
|
|
3526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1948
3527
|
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
3528
|
+
function inferFormatFromPath(
|
|
3529
|
+
path: string | undefined,
|
|
3530
|
+
defaultFormat: string = "jsonl"
|
|
3531
|
+
): "csv" | "jsonl" | "parquet" {
|
|
3532
|
+
if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
1953
3533
|
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
isTemp = true;
|
|
1961
|
-
}
|
|
3534
|
+
const ext = path.toLowerCase().split(".").pop();
|
|
3535
|
+
if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
|
|
3536
|
+
return ext;
|
|
3537
|
+
}
|
|
3538
|
+
return defaultFormat as "csv" | "jsonl" | "parquet";
|
|
3539
|
+
}
|
|
1962
3540
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
format: "jsonl",
|
|
1967
|
-
});
|
|
3541
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3542
|
+
// Dataset List Command (tabular: remote + local in one table)
|
|
3543
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1968
3544
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
3545
|
+
interface DatasetRow {
|
|
3546
|
+
source: string;
|
|
3547
|
+
id: string;
|
|
3548
|
+
name: string;
|
|
3549
|
+
version: string;
|
|
3550
|
+
type: string;
|
|
3551
|
+
samples: string;
|
|
3552
|
+
created: string;
|
|
3553
|
+
[key: string]: string;
|
|
3554
|
+
}
|
|
1973
3555
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
};
|
|
1979
|
-
uploadedDatasets.push(uploadedRef);
|
|
1980
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
1981
|
-
idx === i ? { ...s, status: "done" as const, uploadedRef } : s
|
|
1982
|
-
));
|
|
1983
|
-
} else {
|
|
1984
|
-
hasError = true;
|
|
1985
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
1986
|
-
idx === i ? { ...s, status: "error" as const, error: result.error } : s
|
|
1987
|
-
));
|
|
1988
|
-
}
|
|
1989
|
-
} catch (err) {
|
|
1990
|
-
hasError = true;
|
|
1991
|
-
setUploadStatuses(prev => prev.map((s, idx) =>
|
|
1992
|
-
idx === i ? { ...s, status: "error" as const, error: err instanceof Error ? err.message : String(err) } : s
|
|
1993
|
-
));
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
3556
|
+
function shortDatasetId(id?: string): string {
|
|
3557
|
+
if (!id) return "—";
|
|
3558
|
+
return id.length > 8 ? id.slice(0, 8) : id;
|
|
3559
|
+
}
|
|
1996
3560
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
3561
|
+
function formatSampleCount(n?: number): string {
|
|
3562
|
+
if (typeof n !== "number" || Number.isNaN(n)) return "—";
|
|
3563
|
+
return n.toLocaleString();
|
|
3564
|
+
}
|
|
2003
3565
|
|
|
2004
|
-
|
|
2005
|
-
|
|
3566
|
+
const DATASET_COLUMNS: TableColumn<DatasetRow>[] = [
|
|
3567
|
+
{ key: "source", header: "SOURCE", minWidth: 6, maxWidth: 6, color: (row) => (row.source === "remote" ? "cyan" : "green") },
|
|
3568
|
+
{ key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
|
|
3569
|
+
{ key: "name", header: "NAME", minWidth: 10, maxWidth: 38, flexible: true },
|
|
3570
|
+
{ key: "version", header: "VERSION", minWidth: 4, maxWidth: 8 },
|
|
3571
|
+
{ key: "type", header: "TYPE", minWidth: 4, maxWidth: 14, flexible: true },
|
|
3572
|
+
{ key: "samples", header: "SAMPLES", minWidth: 7, maxWidth: 10 },
|
|
3573
|
+
{ key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
|
|
3574
|
+
];
|
|
2006
3575
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
3576
|
+
function DatasetListCommand() {
|
|
3577
|
+
const { exit } = useApp();
|
|
3578
|
+
const [state, setState] = useState<"loading" | "done">("loading");
|
|
3579
|
+
const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
|
|
3580
|
+
const [remoteError, setRemoteError] = useState<string | null>(null);
|
|
3581
|
+
const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
|
|
2013
3582
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
3583
|
+
useEffect(() => {
|
|
3584
|
+
(async () => {
|
|
3585
|
+
const result = await api.listDatasets();
|
|
3586
|
+
setLocalDatasets(listLocalDatasets());
|
|
3587
|
+
if (result.ok) setRemoteData(result.data ?? null);
|
|
3588
|
+
else setRemoteError(result.error ?? "Unknown error");
|
|
3589
|
+
setState("done");
|
|
2021
3590
|
setTimeout(() => exit(), 500);
|
|
2022
3591
|
})();
|
|
2023
|
-
}, [
|
|
2024
|
-
|
|
2025
|
-
if (state === "resolving") {
|
|
2026
|
-
return <Loading message="Resolving datasets..." />;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
if (state === "uploading") {
|
|
2030
|
-
return (
|
|
2031
|
-
<Box flexDirection="column">
|
|
2032
|
-
<Text>
|
|
2033
|
-
<Text color="blue"><Spinner type="dots" /></Text>
|
|
2034
|
-
{" "}Uploading local datasets...
|
|
2035
|
-
</Text>
|
|
2036
|
-
{uploadStatuses.map((status, idx) => (
|
|
2037
|
-
<Box key={idx}>
|
|
2038
|
-
<Text>
|
|
2039
|
-
{" "}
|
|
2040
|
-
{status.status === "pending" && <Text color="gray">○</Text>}
|
|
2041
|
-
{status.status === "uploading" && <Text color="yellow"><Spinner type="dots" /></Text>}
|
|
2042
|
-
{status.status === "done" && <Text color="green">✓</Text>}
|
|
2043
|
-
{status.status === "error" && <Text color="red">✗</Text>}
|
|
2044
|
-
{" "}{status.name}
|
|
2045
|
-
{status.status === "done" && status.uploadedRef && (
|
|
2046
|
-
<Text color="gray"> → {status.uploadedRef.name}:{status.uploadedRef.version}</Text>
|
|
2047
|
-
)}
|
|
2048
|
-
{status.status === "error" && status.error && (
|
|
2049
|
-
<Text color="red"> ({status.error})</Text>
|
|
2050
|
-
)}
|
|
2051
|
-
</Text>
|
|
2052
|
-
</Box>
|
|
2053
|
-
))}
|
|
2054
|
-
</Box>
|
|
2055
|
-
);
|
|
2056
|
-
}
|
|
3592
|
+
}, [exit]);
|
|
2057
3593
|
|
|
2058
|
-
if (state === "
|
|
2059
|
-
return <Loading message="Creating training job..." />;
|
|
2060
|
-
}
|
|
3594
|
+
if (state === "loading") return <Loading />;
|
|
2061
3595
|
|
|
2062
|
-
|
|
3596
|
+
const remoteDatasets = remoteData?.datasets ?? [];
|
|
3597
|
+
const remoteRows: DatasetRow[] = remoteDatasets.map((ds) => ({
|
|
3598
|
+
source: "remote",
|
|
3599
|
+
id: shortDatasetId(ds.id),
|
|
3600
|
+
name: ds.dataset_name || "—",
|
|
3601
|
+
version: ds.version_number ? `v${ds.version_number}` : "v1",
|
|
3602
|
+
type: ds.dataset_type || "—",
|
|
3603
|
+
samples: formatSampleCount(ds.sample_size),
|
|
3604
|
+
created: relativeTime(ds.created_at),
|
|
3605
|
+
}));
|
|
3606
|
+
const localRows: DatasetRow[] = localDatasets.map((ds) => ({
|
|
3607
|
+
source: "local",
|
|
3608
|
+
id: "—",
|
|
3609
|
+
name: ds.name,
|
|
3610
|
+
version: "—",
|
|
3611
|
+
type: ds.type || "—",
|
|
3612
|
+
samples: formatSampleCount(ds.sample_size),
|
|
3613
|
+
created: "—",
|
|
3614
|
+
}));
|
|
3615
|
+
const rows = [...remoteRows, ...localRows];
|
|
3616
|
+
|
|
3617
|
+
if (rows.length === 0 && !remoteError) {
|
|
2063
3618
|
return (
|
|
2064
3619
|
<Box flexDirection="column">
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
<Text color="green">✓ Uploaded datasets:</Text>
|
|
2068
|
-
{uploadStatuses.filter(s => s.status === "done").map((status, idx) => (
|
|
2069
|
-
<Text key={idx} color="gray">
|
|
2070
|
-
{" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
|
|
2071
|
-
</Text>
|
|
2072
|
-
))}
|
|
2073
|
-
</Box>
|
|
2074
|
-
)}
|
|
2075
|
-
<ErrorMessage error={error} />
|
|
3620
|
+
<Text>No datasets found.</Text>
|
|
3621
|
+
<Text dimColor>Use the Pioneer web app at agent.pioneer.ai to create datasets, or drop JSON/JSONL files in ./datasets/.</Text>
|
|
2076
3622
|
</Box>
|
|
2077
3623
|
);
|
|
2078
3624
|
}
|
|
2079
3625
|
|
|
2080
3626
|
return (
|
|
2081
3627
|
<Box flexDirection="column">
|
|
2082
|
-
{
|
|
2083
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
<Text key={idx} color="gray">
|
|
2087
|
-
{" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
|
|
2088
|
-
</Text>
|
|
3628
|
+
{remoteError ? (
|
|
3629
|
+
<Box flexDirection="column" marginBottom={rows.length ? 1 : 0}>
|
|
3630
|
+
{remoteError.split("\n").map((line, idx) => (
|
|
3631
|
+
<Text key={idx} color="red">{line}</Text>
|
|
2089
3632
|
))}
|
|
2090
3633
|
</Box>
|
|
2091
|
-
)}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
{" "}Model: <Text color="yellow">{jobResult.model_name}</Text>
|
|
2100
|
-
</Text>
|
|
2101
|
-
<Text>
|
|
2102
|
-
{" "}Status: <Text color="blue">{jobResult.status}</Text>
|
|
2103
|
-
</Text>
|
|
2104
|
-
</Box>
|
|
2105
|
-
)}
|
|
3634
|
+
) : null}
|
|
3635
|
+
{rows.length > 0 ? (
|
|
3636
|
+
<DataTable
|
|
3637
|
+
columns={DATASET_COLUMNS}
|
|
3638
|
+
rows={rows}
|
|
3639
|
+
footer={`${remoteRows.length} remote · ${localRows.length} local · use \`pioneer dataset get <name[:version]>\` for details · use \`--json\` for raw JSON`}
|
|
3640
|
+
/>
|
|
3641
|
+
) : null}
|
|
2106
3642
|
</Box>
|
|
2107
3643
|
);
|
|
2108
3644
|
}
|
|
2109
3645
|
|
|
3646
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3647
|
+
// Dataset Get Command (single-resource key/value layout)
|
|
3648
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3649
|
+
|
|
3650
|
+
function datasetToDetailRows(ds: api.Dataset): DetailRow[] {
|
|
3651
|
+
const rows: DetailRow[] = [
|
|
3652
|
+
{ label: "ID", value: formatNonEmpty(ds.id) },
|
|
3653
|
+
{ label: "Name", value: formatNonEmpty(ds.dataset_name) },
|
|
3654
|
+
{ label: "Version", value: ds.version_number ? `v${ds.version_number}` : "—" },
|
|
3655
|
+
{ label: "Type", value: formatNonEmpty(ds.dataset_type) },
|
|
3656
|
+
{ label: "Status", value: formatNonEmpty(ds.status), valueColor: statusColor(ds.status) },
|
|
3657
|
+
{ label: "Samples", value: formatSampleCount(ds.sample_size) },
|
|
3658
|
+
{ label: "Train ratio", value: ds.train_ratio != null ? formatPercent(ds.train_ratio) : "—" },
|
|
3659
|
+
{ label: "Visibility", value: formatNonEmpty(ds.visibility) },
|
|
3660
|
+
{ label: "Labels", value: formatLabels(ds.labels) },
|
|
3661
|
+
{ label: "Annotation status", value: formatNonEmpty(ds.annotation_status) },
|
|
3662
|
+
{ label: "Project ID", value: formatNonEmpty(ds.project_id) },
|
|
3663
|
+
{ label: "Root dataset ID", value: formatNonEmpty(ds.root_dataset_id) },
|
|
3664
|
+
{ label: "Created", value: formatTimestamp(ds.created_at) },
|
|
3665
|
+
{ label: "Updated", value: formatTimestamp(ds.updated_at) },
|
|
3666
|
+
];
|
|
3667
|
+
if (ds.processing_error) {
|
|
3668
|
+
rows.push({ label: "Error", value: ds.processing_error, valueColor: "red" });
|
|
3669
|
+
}
|
|
3670
|
+
return rows;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
function DatasetGetCommand({ dataset }: { dataset: api.DatasetRef }) {
|
|
3674
|
+
const { exit } = useApp();
|
|
3675
|
+
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
3676
|
+
const [data, setData] = useState<api.Dataset | null>(null);
|
|
3677
|
+
const [error, setError] = useState("");
|
|
3678
|
+
|
|
3679
|
+
useEffect(() => {
|
|
3680
|
+
let active = true;
|
|
3681
|
+
(async () => {
|
|
3682
|
+
const result = await api.getDataset(dataset);
|
|
3683
|
+
if (!active) return;
|
|
3684
|
+
if (!result.ok) {
|
|
3685
|
+
setError(result.error ?? "Failed to load dataset.");
|
|
3686
|
+
setState("error");
|
|
3687
|
+
} else {
|
|
3688
|
+
setData(result.data ?? null);
|
|
3689
|
+
setState("done");
|
|
3690
|
+
}
|
|
3691
|
+
setTimeout(() => exit(), 500);
|
|
3692
|
+
})();
|
|
3693
|
+
return () => {
|
|
3694
|
+
active = false;
|
|
3695
|
+
};
|
|
3696
|
+
}, [dataset.name, dataset.version, exit]);
|
|
3697
|
+
|
|
3698
|
+
if (state === "loading") return <Loading message={`Loading dataset ${dataset.name}:${dataset.version}...`} />;
|
|
3699
|
+
if (state === "error") return <ErrorMessage error={error} />;
|
|
3700
|
+
if (!data) return <ErrorMessage error="Dataset response was empty." />;
|
|
3701
|
+
|
|
3702
|
+
return (
|
|
3703
|
+
<DetailView
|
|
3704
|
+
rows={datasetToDetailRows(data)}
|
|
3705
|
+
footer={`Use \`pioneer dataset get ${dataset.name}:${dataset.version} --json\` for the raw JSON payload.`}
|
|
3706
|
+
/>
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
2110
3710
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2111
3711
|
// Dataset Download Command
|
|
2112
3712
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2282,13 +3882,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
|
|
|
2282
3882
|
<Box width={24}>
|
|
2283
3883
|
<Text color="blue">{datasetInfo.substring(0, 22)}</Text>
|
|
2284
3884
|
</Box>
|
|
2285
|
-
<Box width={14}>
|
|
2286
|
-
<Text>
|
|
2287
|
-
<Text color="yellow">{model.nr_epochs}</Text>
|
|
2288
|
-
<Text dimColor>e </Text>
|
|
2289
|
-
<Text color="yellow">{model.learning_rate}</Text>
|
|
2290
|
-
</Text>
|
|
2291
|
-
</Box>
|
|
2292
3885
|
<Box width={30}>
|
|
2293
3886
|
<Text color="green">{metricsDisplay || "N/A"}</Text>
|
|
2294
3887
|
</Box>
|
|
@@ -2299,11 +3892,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
|
|
|
2299
3892
|
<Text dimColor>{formatDateShort(model.completed_at || model.started_at || model.created_at)}</Text>
|
|
2300
3893
|
</Box>
|
|
2301
3894
|
</Box>
|
|
2302
|
-
{model.error_message && (
|
|
2303
|
-
<Box marginLeft={2}>
|
|
2304
|
-
<Text color="red">Error: {model.error_message}</Text>
|
|
2305
|
-
</Box>
|
|
2306
|
-
)}
|
|
2307
3895
|
</Box>
|
|
2308
3896
|
);
|
|
2309
3897
|
}
|
|
@@ -2375,15 +3963,33 @@ function DeployedModelCard({ model, index }: DeployedModelCardProps) {
|
|
|
2375
3963
|
interface ProjectModelCardProps {
|
|
2376
3964
|
model: api.ProjectResponse;
|
|
2377
3965
|
index: number;
|
|
3966
|
+
jobIndex?: Map<string, api.TrainingJob>;
|
|
2378
3967
|
}
|
|
2379
3968
|
|
|
2380
|
-
function ProjectModelCard({ model, index }: ProjectModelCardProps) {
|
|
3969
|
+
function ProjectModelCard({ model, index, jobIndex }: ProjectModelCardProps) {
|
|
2381
3970
|
const formatDateShort = (dateStr: string | null | undefined) => {
|
|
2382
3971
|
if (!dateStr) return "N/A";
|
|
2383
3972
|
const date = new Date(dateStr);
|
|
2384
3973
|
return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
2385
3974
|
};
|
|
2386
3975
|
|
|
3976
|
+
// Drop "<org>/" prefix from HF model ids for compact list display.
|
|
3977
|
+
const stripOrg = (modelId: string) => {
|
|
3978
|
+
const idx = modelId.indexOf("/");
|
|
3979
|
+
return idx >= 0 ? modelId.slice(idx + 1) : modelId;
|
|
3980
|
+
};
|
|
3981
|
+
const selected = model.selected_model_id ?? "";
|
|
3982
|
+
const activeJob = selected && UUID_REGEX.test(selected) ? jobIndex?.get(selected) : undefined;
|
|
3983
|
+
const selectedDisplay = (() => {
|
|
3984
|
+
if (!selected) return "N/A";
|
|
3985
|
+
if (activeJob) {
|
|
3986
|
+
const name = activeJob.model_name ?? shortJobId(selected);
|
|
3987
|
+
const base = activeJob.base_model ? ` ← ${stripOrg(activeJob.base_model)}` : "";
|
|
3988
|
+
return `${name}${base}`;
|
|
3989
|
+
}
|
|
3990
|
+
return UUID_REGEX.test(selected) ? shortJobId(selected) : stripOrg(selected);
|
|
3991
|
+
})();
|
|
3992
|
+
|
|
2387
3993
|
return (
|
|
2388
3994
|
<Box flexDirection="column" marginTop={index > 0 ? 0 : 0}>
|
|
2389
3995
|
<Box>
|
|
@@ -2395,8 +4001,8 @@ function ProjectModelCard({ model, index }: ProjectModelCardProps) {
|
|
|
2395
4001
|
<Box width={38}>
|
|
2396
4002
|
<Text dimColor>{model.id}</Text>
|
|
2397
4003
|
</Box>
|
|
2398
|
-
<Box width={
|
|
2399
|
-
<Text color="magenta"
|
|
4004
|
+
<Box width={36}>
|
|
4005
|
+
<Text color="magenta" wrap="truncate-end">{selectedDisplay}</Text>
|
|
2400
4006
|
</Box>
|
|
2401
4007
|
<Box width={18}>
|
|
2402
4008
|
<Text dimColor>{formatDateShort(model.created_at)}</Text>
|
|
@@ -2425,21 +4031,41 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
2425
4031
|
const { exit } = useApp();
|
|
2426
4032
|
const [state, setState] = useState<"loading" | "done" | "error">("loading");
|
|
2427
4033
|
const [data, setData] = useState<api.AllModelsResponse | null>(null);
|
|
4034
|
+
const [jobIndex, setJobIndex] = useState<Map<string, api.TrainingJob>>(new Map());
|
|
2428
4035
|
const [error, setError] = useState("");
|
|
2429
4036
|
|
|
2430
4037
|
useEffect(() => {
|
|
2431
4038
|
(async () => {
|
|
2432
4039
|
const result = await api.listAllModels();
|
|
2433
|
-
if (result.ok) {
|
|
2434
|
-
setData(result.data ?? null);
|
|
2435
|
-
setState("done");
|
|
2436
|
-
} else {
|
|
4040
|
+
if (!result.ok) {
|
|
2437
4041
|
setError(result.error ?? "Unknown error");
|
|
2438
4042
|
setState("error");
|
|
4043
|
+
setTimeout(() => exit(), 500);
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
setData(result.data ?? null);
|
|
4047
|
+
|
|
4048
|
+
// For "registered" (Model Entries), each project's selected_model_id may
|
|
4049
|
+
// be a training-job UUID. Pull all jobs once and build a lookup so we
|
|
4050
|
+
// can render `model_name (base_model)` in place of the bare UUID.
|
|
4051
|
+
const projects = result.data?.projects ?? [];
|
|
4052
|
+
const needsHydration = projects.some((p) =>
|
|
4053
|
+
p.selected_model_id && UUID_REGEX.test(p.selected_model_id),
|
|
4054
|
+
);
|
|
4055
|
+
if (filter === "registered" && needsHydration) {
|
|
4056
|
+
const jobsResult = await api.listJobs();
|
|
4057
|
+
if (jobsResult.ok) {
|
|
4058
|
+
const map = new Map<string, api.TrainingJob>();
|
|
4059
|
+
for (const j of jobsResult.data?.training_jobs ?? []) {
|
|
4060
|
+
if (j.id) map.set(j.id, j);
|
|
4061
|
+
}
|
|
4062
|
+
setJobIndex(map);
|
|
4063
|
+
}
|
|
2439
4064
|
}
|
|
4065
|
+
setState("done");
|
|
2440
4066
|
setTimeout(() => exit(), 500);
|
|
2441
4067
|
})();
|
|
2442
|
-
}, [exit]);
|
|
4068
|
+
}, [exit, filter]);
|
|
2443
4069
|
|
|
2444
4070
|
if (state === "loading") {
|
|
2445
4071
|
return <Loading />;
|
|
@@ -2470,15 +4096,20 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
2470
4096
|
<Box width={38}>
|
|
2471
4097
|
<Text bold dimColor>Model ID</Text>
|
|
2472
4098
|
</Box>
|
|
2473
|
-
<Box width={
|
|
2474
|
-
<Text bold dimColor>
|
|
4099
|
+
<Box width={36}>
|
|
4100
|
+
<Text bold dimColor>Active Model</Text>
|
|
2475
4101
|
</Box>
|
|
2476
4102
|
<Box width={18}>
|
|
2477
4103
|
<Text bold dimColor>Created</Text>
|
|
2478
4104
|
</Box>
|
|
2479
4105
|
</Box>
|
|
2480
4106
|
{data?.projects.map((model, index) => (
|
|
2481
|
-
<ProjectModelCard
|
|
4107
|
+
<ProjectModelCard
|
|
4108
|
+
key={model.id || index}
|
|
4109
|
+
model={model}
|
|
4110
|
+
index={index}
|
|
4111
|
+
jobIndex={jobIndex}
|
|
4112
|
+
/>
|
|
2482
4113
|
))}
|
|
2483
4114
|
</Box>
|
|
2484
4115
|
)}
|
|
@@ -2539,9 +4170,6 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
|
|
|
2539
4170
|
<Box width={24}>
|
|
2540
4171
|
<Text bold dimColor>Dataset</Text>
|
|
2541
4172
|
</Box>
|
|
2542
|
-
<Box width={14}>
|
|
2543
|
-
<Text bold dimColor>Config</Text>
|
|
2544
|
-
</Box>
|
|
2545
4173
|
<Box width={30}>
|
|
2546
4174
|
<Text bold dimColor>Metrics</Text>
|
|
2547
4175
|
</Box>
|
|
@@ -2604,7 +4232,7 @@ function normalizeModelId(modelId: string): string {
|
|
|
2604
4232
|
|
|
2605
4233
|
function getDecoderTaskType(model: api.BaseModelInfo | null | undefined): string {
|
|
2606
4234
|
if (!model) return "";
|
|
2607
|
-
return `${model.task_type ??
|
|
4235
|
+
return `${model.task_type ?? ""}`.trim().toLowerCase();
|
|
2608
4236
|
}
|
|
2609
4237
|
|
|
2610
4238
|
function modelSupportsDecoderInference(model: api.BaseModelInfo | null | undefined): boolean {
|
|
@@ -2837,7 +4465,9 @@ type HelpContext =
|
|
|
2837
4465
|
| "eval"
|
|
2838
4466
|
| "benchmark"
|
|
2839
4467
|
| "inference"
|
|
2840
|
-
| "agent"
|
|
4468
|
+
| "agent"
|
|
4469
|
+
| "model-endpoints"
|
|
4470
|
+
| "model-artifacts";
|
|
2841
4471
|
|
|
2842
4472
|
interface HelpProps {
|
|
2843
4473
|
context?: HelpContext;
|
|
@@ -2879,112 +4509,11 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
2879
4509
|
<Text dimColor> Dataset format: name[:version] (version defaults to "latest")</Text>
|
|
2880
4510
|
<Text dimColor> Examples: my-dataset, my-dataset:v1, my-dataset:latest</Text>
|
|
2881
4511
|
<Text> </Text>
|
|
2882
|
-
<Text> dataset list
|
|
2883
|
-
<Text> dataset get {"<name[:version]>"} Get dataset details
|
|
2884
|
-
<Text> dataset delete {"<name[:version]>"} Delete a dataset</Text>
|
|
2885
|
-
<Text> dataset analyze {"<name[:version]>"} Analyze a dataset</Text>
|
|
2886
|
-
<Text> dataset analyze-llm {"<name[:version]>"} LLM-only dataset analysis</Text>
|
|
4512
|
+
<Text> dataset list List all datasets (tabular). Add --json for raw JSON.</Text>
|
|
4513
|
+
<Text> dataset get {"<name[:version]>"} Get dataset details (key/value layout). Add --json for raw JSON.</Text>
|
|
2887
4514
|
<Text> </Text>
|
|
2888
|
-
<Text
|
|
2889
|
-
<Text>
|
|
2890
|
-
<Text> --labels {"<l1,l2,...>"} Entity labels (required)</Text>
|
|
2891
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
2892
|
-
<Text> --domain {"<desc>"} Domain description</Text>
|
|
2893
|
-
<Text> --save true Save to database</Text>
|
|
2894
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
2895
|
-
<Text> </Text>
|
|
2896
|
-
<Text> dataset generate classification</Text>
|
|
2897
|
-
<Text> --labels {"<l1,l2,...>"} Class labels (required)</Text>
|
|
2898
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
2899
|
-
<Text> --domain {"<desc>"} Domain description</Text>
|
|
2900
|
-
<Text> --multi-label true Enable multi-label</Text>
|
|
2901
|
-
<Text> --save true Save to database</Text>
|
|
2902
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
2903
|
-
<Text> </Text>
|
|
2904
|
-
<Text> dataset generate custom</Text>
|
|
2905
|
-
<Text> --prompt {"<prompt>"} Task description (required)</Text>
|
|
2906
|
-
<Text> --format {"<json>"} Output format as JSON (required)</Text>
|
|
2907
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
2908
|
-
<Text> --save true Save to database</Text>
|
|
2909
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
2910
|
-
<Text> </Text>
|
|
2911
|
-
<Text> dataset generate decoder</Text>
|
|
2912
|
-
<Text> --domain {"<desc>"} Domain/task description (required)</Text>
|
|
2913
|
-
<Text> --instruction {"<text>"} System instruction (optional, auto-inferred)</Text>
|
|
2914
|
-
<Text> --num {"<n>"} Number of examples (default: 10)</Text>
|
|
2915
|
-
<Text> --save true Save to database</Text>
|
|
2916
|
-
<Text> --name {"<name>"} Dataset name (required if --save)</Text>
|
|
2917
|
-
<Text> Advanced generation flags:</Text>
|
|
2918
|
-
<Text> --quality {"<light|medium|heavy>"} Generation quality profile</Text>
|
|
2919
|
-
<Text> --generation-profile {"<auto|fast|balanced|quality>"} Runtime profile</Text>
|
|
2920
|
-
<Text> --reasoning-trace {"true|false"} Include reasoning traces (decoder only)</Text>
|
|
2921
|
-
<Text> --reasoning-effort {"<low|medium|high>"} Reasoning effort</Text>
|
|
2922
|
-
<Text> --multiplicator {"<json>"} Multiplicator settings</Text>
|
|
2923
|
-
<Text> --use-meta-felix {"true|false"} Use MetaFelix metadata</Text>
|
|
2924
|
-
<Text> --min-criteria {"<n>"} Minimum diversity criteria</Text>
|
|
2925
|
-
<Text> --target-choices {"<n>"} Diversity target choices</Text>
|
|
2926
|
-
<Text> --project-id {"<id>"} Project ID</Text>
|
|
2927
|
-
<Text> --type {"training|evaluation|split"} Dataset type</Text>
|
|
2928
|
-
<Text> --visibility {"private|public"} Dataset visibility</Text>
|
|
2929
|
-
<Text> --split-ratio {"<train:eval>|{json>}"} Split dataset ratio</Text>
|
|
2930
|
-
<Text> --negative-ratio {"<n>"} Percent negative samples</Text>
|
|
2931
|
-
<Text> --classified-examples {"<json>"} Classified examples with feedback</Text>
|
|
2932
|
-
<Text> </Text>
|
|
2933
|
-
<Text bold> Infer Labels:</Text>
|
|
2934
|
-
<Text> dataset infer ner Infer NER labels from description</Text>
|
|
2935
|
-
<Text> dataset infer classification Infer classification labels</Text>
|
|
2936
|
-
<Text> dataset infer fields Infer input/output fields</Text>
|
|
2937
|
-
<Text> --domain {"<desc>"} Domain description (required)</Text>
|
|
2938
|
-
<Text> dataset infer infer-advanced Infer constraints and multiplicator from a prompt</Text>
|
|
2939
|
-
<Text> --prompt {"<prompt>"} Prompt for inference</Text>
|
|
2940
|
-
<Text> --labels {"<l1,l2,...>"} Optional labels to guide suggestions</Text>
|
|
2941
|
-
<Text> --data-type {"<type>"} entity_extraction|classification|json_extraction</Text>
|
|
2942
|
-
<Text> dataset infer improve-prompt Improve a generation prompt</Text>
|
|
2943
|
-
<Text> --prompt {"<prompt>"} Prompt to improve</Text>
|
|
2944
|
-
<Text> --data-type {"<type>"} Optional prompt domain hint</Text>
|
|
2945
|
-
<Text> dataset label-existing ner Label existing NER texts</Text>
|
|
2946
|
-
<Text> --labels {"<l1,l2,...>"} Labels for entities</Text>
|
|
2947
|
-
<Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
|
|
2948
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
2949
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
2950
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
2951
|
-
<Text> dataset label-existing classification Label existing classification texts</Text>
|
|
2952
|
-
<Text> --labels {"<l1,l2,...>"} Labels for classes</Text>
|
|
2953
|
-
<Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
|
|
2954
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
2955
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
2956
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
2957
|
-
<Text> dataset label-existing fields Label existing structured records</Text>
|
|
2958
|
-
<Text> --input-fields {"[{\"name\":\"...\"},...]"} Input schema fields (required)</Text>
|
|
2959
|
-
<Text> --output-fields {"[{\"name\":\"...\"},...]"} Output schema fields (required)</Text>
|
|
2960
|
-
<Text> --inputs {"[{\"f1\":\"v\"},...]"} Input records JSON array (required)</Text>
|
|
2961
|
-
<Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
|
|
2962
|
-
<Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
|
|
2963
|
-
<Text> --save {"<true|false>"} Save dataset (default: false)</Text>
|
|
2964
|
-
<Text> </Text>
|
|
2965
|
-
<Text bold> Upload/Download:</Text>
|
|
2966
|
-
<Text> dataset upload {"<file>"} Upload local file to Pioneer</Text>
|
|
2967
|
-
<Text> --name {"<name>"} Dataset name (required)</Text>
|
|
2968
|
-
<Text> --type {"<type>"} Type: ner, classification, custom</Text>
|
|
2969
|
-
<Text> dataset upload {"<name[:version]>"} --to hf Upload Pioneer dataset to Hugging Face</Text>
|
|
2970
|
-
<Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
|
|
2971
|
-
<Text> --private Make repo private</Text>
|
|
2972
|
-
<Text dimColor> Note: Set HF token with 'pioneer auth hf' first</Text>
|
|
2973
|
-
<Text> dataset download {"<name[:version]>"} Download from Pioneer to local file</Text>
|
|
2974
|
-
<Text> --format {"<type>"} Format: jsonl, csv, parquet (default: jsonl)</Text>
|
|
2975
|
-
<Text> --output {"<path>"} Output file path</Text>
|
|
2976
|
-
<Text> dataset download --from hf Download from Hugging Face to Pioneer</Text>
|
|
2977
|
-
<Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
|
|
2978
|
-
<Text> --name {"<name>"} Local dataset name (optional)</Text>
|
|
2979
|
-
<Text> --revision {"<rev>"} Git revision/branch (optional)</Text>
|
|
2980
|
-
<Text dimColor> Note: For private repos, set HF token with 'pioneer auth hf'</Text>
|
|
2981
|
-
<Text> </Text>
|
|
2982
|
-
<Text bold> Data Editing:</Text>
|
|
2983
|
-
<Text> dataset edit --help Show data editing commands</Text>
|
|
2984
|
-
<Text> dataset edit scan-pii {"<name[:version]>"} Scan for PII</Text>
|
|
2985
|
-
<Text> dataset edit dismiss-outlier {"<name[:version]>"} Dismiss an outlier fingerprint</Text>
|
|
2986
|
-
<Text> --fingerprint {"<hash>"} Outlier fingerprint from dataset analysis</Text>
|
|
2987
|
-
<Text> dataset edit subsample {"<name[:version]>"} Create a subsample</Text>
|
|
4515
|
+
<Text dimColor> Other dataset subcommands (generate/edit/upload/etc.) are temporarily hidden in this version.</Text>
|
|
4516
|
+
<Text dimColor> Use the Pioneer web app at agent.pioneer.ai to create or edit datasets.</Text>
|
|
2988
4517
|
</Box>
|
|
2989
4518
|
);
|
|
2990
4519
|
}
|
|
@@ -3047,18 +4576,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3047
4576
|
return (
|
|
3048
4577
|
<Box flexDirection="column">
|
|
3049
4578
|
<Text bold>Job Commands:</Text>
|
|
3050
|
-
<Text> job list List training jobs
|
|
3051
|
-
<Text> job get {"<id>"} Get job details
|
|
4579
|
+
<Text> job list List training jobs (tabular). Add --json for raw JSON.</Text>
|
|
4580
|
+
<Text> job get {"<id>"} Get job details (key/value layout). Add --json for raw JSON. Accepts a unique short prefix from `job list`.</Text>
|
|
3052
4581
|
<Text> job logs {"<id>"} Get job logs</Text>
|
|
3053
4582
|
<Text> job delete {"<id>"} Delete a training job</Text>
|
|
3054
|
-
<Text>
|
|
3055
|
-
<Text
|
|
3056
|
-
<Text
|
|
3057
|
-
<Text> remote:{"<name>"}:{"<version>"} - by name and version</Text>
|
|
3058
|
-
<Text> remote:{"<uuid>"} - by UUID</Text>
|
|
3059
|
-
<Text> local:{"<name>"} - auto-upload from ./datasets/</Text>
|
|
3060
|
-
<Text> --base-model {"<model>"} Base model (default: fastino/gliner2-base-v1)</Text>
|
|
3061
|
-
<Text> --epochs {"<n>"} Number of epochs (default: 5)</Text>
|
|
4583
|
+
<Text> </Text>
|
|
4584
|
+
<Text dimColor> To create a training job, use `pioneer agent` — it will help you pick a base model,</Text>
|
|
4585
|
+
<Text dimColor> select datasets, and configure training conversationally.</Text>
|
|
3062
4586
|
</Box>
|
|
3063
4587
|
);
|
|
3064
4588
|
}
|
|
@@ -3077,13 +4601,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3077
4601
|
<Text> --description {"<text>"} Optional</Text>
|
|
3078
4602
|
<Text> --model {"<base-model-id>"} Optional (starts interactive picker when omitted)</Text>
|
|
3079
4603
|
<Text> --example {"<json>"} Optional</Text>
|
|
3080
|
-
<Text> model endpoints get {"<model-id>"} Get endpoint/
|
|
4604
|
+
<Text> model endpoints get {"<model-id>"} Get endpoint details (key/value layout, includes dataset count). Add --json for raw JSON.</Text>
|
|
3081
4605
|
<Text> model endpoints update {"<model-id>"} Update endpoint metadata</Text>
|
|
3082
4606
|
<Text> --name {"<name>"} --icon {"<icon>"} --repo {"<repo-url>"} --description {"<text>"} --model-id {"<id>"}</Text>
|
|
3083
4607
|
<Text> model endpoints delete {"<model-id>"} Delete an endpoint/model entry</Text>
|
|
3084
|
-
<Text> model endpoints
|
|
3085
|
-
<Text> model endpoints
|
|
3086
|
-
<Text
|
|
4608
|
+
<Text> model endpoints quality-metrics {"<model-id>"} Show LLMAJ pass/fail metrics. Add --json for raw JSON.</Text>
|
|
4609
|
+
<Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
|
|
4610
|
+
<Text dimColor> If --job is omitted, pick from a list of deployable jobs interactively. By default the list is filtered to jobs whose base model matches the endpoint; pass --all to bypass that filter.</Text>
|
|
3087
4611
|
<Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"} Rollback endpoint to previous deployment</Text>
|
|
3088
4612
|
</Box>
|
|
3089
4613
|
);
|
|
@@ -3098,7 +4622,7 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3098
4622
|
<Text> model artifacts list Show both trained and deployed artifacts</Text>
|
|
3099
4623
|
<Text> model artifacts trained List trained artifacts</Text>
|
|
3100
4624
|
<Text> model artifacts deployed List deployed artifacts</Text>
|
|
3101
|
-
<Text> model artifacts download {"<job-id>"}
|
|
4625
|
+
<Text> model artifacts download {"<job-id>"} Get a signed download URL for the artifact (key/value layout). Add --json for raw JSON.</Text>
|
|
3102
4626
|
<Text> model artifacts delete {"<job-id>"} Delete deployed artifact record</Text>
|
|
3103
4627
|
<Text> model artifacts upload {"<job-id>"} --to hf Upload trained model artifact to Hugging Face</Text>
|
|
3104
4628
|
<Text> --repo {"<repo>"} HF repo (required, e.g., username/model)</Text>
|
|
@@ -3116,17 +4640,20 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3116
4640
|
<Text bold>Model Commands:</Text>
|
|
3117
4641
|
<Text> model endpoints ... (alias: model_endpoints) Manage model catalog entries (from /projects)</Text>
|
|
3118
4642
|
<Text> model artifacts ... (alias: model_artifacts) Manage trained/deployed artifacts (from /felix)</Text>
|
|
4643
|
+
<Text> model base-models List available base models (tabular)</Text>
|
|
3119
4644
|
<Text> </Text>
|
|
3120
4645
|
<Text> model endpoints list</Text>
|
|
3121
4646
|
<Text> model endpoints create</Text>
|
|
3122
4647
|
<Text> model endpoints get {"<model-id>"}</Text>
|
|
3123
|
-
<Text> model endpoints
|
|
4648
|
+
<Text> model endpoints quality-metrics {"<model-id>"}</Text>
|
|
4649
|
+
<Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
|
|
3124
4650
|
<Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"}</Text>
|
|
3125
4651
|
<Text> model artifacts list</Text>
|
|
3126
4652
|
<Text> model artifacts trained</Text>
|
|
3127
|
-
|
|
3128
|
-
|
|
4653
|
+
<Text> model artifacts deployed</Text>
|
|
4654
|
+
<Text> model artifacts download {"<job-id>"}</Text>
|
|
3129
4655
|
<Text> </Text>
|
|
4656
|
+
<Text dimColor> Most read commands accept `--json` for the raw JSON payload.</Text>
|
|
3130
4657
|
</Box>
|
|
3131
4658
|
);
|
|
3132
4659
|
}
|
|
@@ -3188,6 +4715,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3188
4715
|
<Text> --mode {"<research>"} --mode research uses Pro workflow</Text>
|
|
3189
4716
|
<Text> </Text>
|
|
3190
4717
|
<Text> Omit --mode to use the default standard interactive mode.</Text>
|
|
4718
|
+
<Text> agent sessions List and resume previous sessions</Text>
|
|
4719
|
+
<Text> agent resume {"[conversation-id]"} List sessions, then resume a selected conversation</Text>
|
|
3191
4720
|
<Text> --conversation-id {"<id>"} Continue an existing conversation</Text>
|
|
3192
4721
|
<Text> --filters {"<json>"} Reserved for future query filters</Text>
|
|
3193
4722
|
<Text> --history {"<json>"} Optional message history JSON</Text>
|
|
@@ -3196,6 +4725,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
|
|
|
3196
4725
|
<Text dimColor>Example: pioneer agent --mode research</Text>
|
|
3197
4726
|
<Text dimColor>Then type: Analyze failures and propose retraining plan</Text>
|
|
3198
4727
|
<Text dimColor>Then type: Draft a short status summary</Text>
|
|
4728
|
+
<Text dimColor>Example: pioneer agent resume</Text>
|
|
4729
|
+
<Text dimColor>Example: pioneer agent resume b042f7a1-0e7e-4f78-96df-a1cc2d4afcdf</Text>
|
|
3199
4730
|
</Box>
|
|
3200
4731
|
);
|
|
3201
4732
|
}
|
|
@@ -3254,13 +4785,8 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3254
4785
|
rest[0] === "create" &&
|
|
3255
4786
|
parseErrors.length === 1 &&
|
|
3256
4787
|
parseErrors[0] === "--model";
|
|
3257
|
-
const isModelEndpointsDeployMissingJob =
|
|
3258
|
-
group === "model" &&
|
|
3259
|
-
normalizedAction === "endpoints" &&
|
|
3260
|
-
rest[0] === "deploy" &&
|
|
3261
|
-
!flags["job"];
|
|
3262
4788
|
|
|
3263
|
-
if (group === "
|
|
4789
|
+
if (group === "inference" || group === "eval" || group === "benchmark") {
|
|
3264
4790
|
return (
|
|
3265
4791
|
<ErrorMessage
|
|
3266
4792
|
error={`The '${group}' command group is temporarily hidden for this version. Use 'pioneer --help' to see available commands.`}
|
|
@@ -3268,35 +4794,64 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3268
4794
|
);
|
|
3269
4795
|
}
|
|
3270
4796
|
|
|
3271
|
-
if (
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
4797
|
+
if (group === "dataset") {
|
|
4798
|
+
const allowedDatasetActions = new Set(["list", "get", "help", undefined, ""]);
|
|
4799
|
+
const subAction = action ?? "";
|
|
4800
|
+
const isHelp = flags["help"] === "true" || subAction === "" || subAction === "help";
|
|
4801
|
+
if (!isHelp && !allowedDatasetActions.has(subAction)) {
|
|
3277
4802
|
return (
|
|
3278
|
-
<
|
|
3279
|
-
|
|
3280
|
-
Promise.resolve<api.ApiResult<{ message: string }>>({
|
|
3281
|
-
ok: false,
|
|
3282
|
-
status: 400,
|
|
3283
|
-
error: errorMessage,
|
|
3284
|
-
data: {
|
|
3285
|
-
message: errorMessage,
|
|
3286
|
-
},
|
|
3287
|
-
})
|
|
3288
|
-
}
|
|
3289
|
-
successMessage="Validation failed"
|
|
4803
|
+
<ErrorMessage
|
|
4804
|
+
error={`'dataset ${subAction}' is temporarily hidden for this version. Available: pioneer dataset list, pioneer dataset get <name[:version]>.`}
|
|
3290
4805
|
/>
|
|
3291
4806
|
);
|
|
3292
4807
|
}
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
if (hasParseErrors && !isModelCreateMissingModel) {
|
|
4811
|
+
const missingValueHints: Record<string, string> = {
|
|
4812
|
+
"--model": "<base-model-id>",
|
|
4813
|
+
"--mode":
|
|
4814
|
+
group === "agent" || group === "agents"
|
|
4815
|
+
? "<research>"
|
|
4816
|
+
: "<value>",
|
|
4817
|
+
"--conversation-id": "<session-id>",
|
|
4818
|
+
"--conversation": "<session-id>",
|
|
4819
|
+
"--history": "<json>",
|
|
4820
|
+
"--filters": "<json>",
|
|
4821
|
+
"--format": "<format>",
|
|
4822
|
+
"--text": "<text>",
|
|
4823
|
+
"--prompt": "<text>",
|
|
4824
|
+
"--name": "<name>",
|
|
4825
|
+
"--repo": "<url>",
|
|
4826
|
+
"--icon": "<icon>",
|
|
4827
|
+
"--description": "<text>",
|
|
4828
|
+
"--api-key": "<key>",
|
|
4829
|
+
"--api-url": "<url>",
|
|
4830
|
+
"--message": "<text>",
|
|
4831
|
+
"--inputs": "<json>",
|
|
4832
|
+
"--labels": "<json-array>",
|
|
4833
|
+
"--label-column": "<column>",
|
|
4834
|
+
"--text-column": "<column>",
|
|
4835
|
+
"--output": "<path>",
|
|
4836
|
+
"--format-results": "<true|false>",
|
|
4837
|
+
"--include-confidence": "<true|false>",
|
|
4838
|
+
"--include-spans": "<true|false>",
|
|
4839
|
+
"--reasoning-trace": "<true|false>",
|
|
4840
|
+
"--reasoning-effort": "<low|medium|high>",
|
|
4841
|
+
};
|
|
4842
|
+
const getValueHint = (flag: string) => {
|
|
4843
|
+
if (flag === "--mode" && group === "agent") {
|
|
4844
|
+
return "<research> (default is standard when omitted)";
|
|
4845
|
+
}
|
|
4846
|
+
return missingValueHints[flag] ?? "<value>";
|
|
4847
|
+
};
|
|
3293
4848
|
|
|
3294
4849
|
return (
|
|
3295
4850
|
<Box flexDirection="column">
|
|
3296
4851
|
<ErrorMessage error="One or more flags are missing values. Please provide values for: " />
|
|
3297
4852
|
{parseErrors.map((flag) => (
|
|
3298
4853
|
<Text dimColor key={flag}>
|
|
3299
|
-
|
|
4854
|
+
- {flag} {getValueHint(flag)}
|
|
3300
4855
|
</Text>
|
|
3301
4856
|
))}
|
|
3302
4857
|
</Box>
|
|
@@ -3654,6 +5209,9 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3654
5209
|
}
|
|
3655
5210
|
|
|
3656
5211
|
if (action === "list") {
|
|
5212
|
+
if (flags.json === "true") {
|
|
5213
|
+
return <ApiCommand action={api.listDatasets} />;
|
|
5214
|
+
}
|
|
3657
5215
|
return <DatasetListCommand />;
|
|
3658
5216
|
}
|
|
3659
5217
|
if (action === "get" && rest[0]) {
|
|
@@ -3661,7 +5219,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
3661
5219
|
if (!dataset) {
|
|
3662
5220
|
return <ErrorMessage error={`Invalid dataset format: ${rest[0]}. Use name[:version] format (e.g., my-dataset or my-dataset:v1).`} />;
|
|
3663
5221
|
}
|
|
3664
|
-
|
|
5222
|
+
if (flags.json === "true") {
|
|
5223
|
+
return <ApiCommand action={() => api.getDataset(dataset)} />;
|
|
5224
|
+
}
|
|
5225
|
+
return <DatasetGetCommand dataset={dataset} />;
|
|
3665
5226
|
}
|
|
3666
5227
|
if (action === "delete" && rest[0]) {
|
|
3667
5228
|
const dataset = parseDatasetRef(rest[0]);
|
|
@@ -4178,48 +5739,55 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4178
5739
|
return <Help context="job" />;
|
|
4179
5740
|
}
|
|
4180
5741
|
if (action === "list") {
|
|
4181
|
-
|
|
5742
|
+
if (flags.json === "true") {
|
|
5743
|
+
return <ApiCommand action={api.listJobs} />;
|
|
5744
|
+
}
|
|
5745
|
+
return <JobListCommand />;
|
|
4182
5746
|
}
|
|
4183
5747
|
if (action === "get" && rest[0]) {
|
|
4184
|
-
|
|
5748
|
+
const wantsJson = flags.json === "true";
|
|
5749
|
+
return (
|
|
5750
|
+
<WithResolvedJobId
|
|
5751
|
+
rawId={rest[0]}
|
|
5752
|
+
render={(jobId) =>
|
|
5753
|
+
wantsJson ? (
|
|
5754
|
+
<ApiCommand action={() => api.getJob(jobId)} />
|
|
5755
|
+
) : (
|
|
5756
|
+
<JobGetCommand jobId={jobId} />
|
|
5757
|
+
)
|
|
5758
|
+
}
|
|
5759
|
+
/>
|
|
5760
|
+
);
|
|
4185
5761
|
}
|
|
4186
5762
|
if (action === "logs" && rest[0]) {
|
|
4187
|
-
return
|
|
5763
|
+
return (
|
|
5764
|
+
<WithResolvedJobId
|
|
5765
|
+
rawId={rest[0]}
|
|
5766
|
+
render={(jobId) => <JobLogsCommand jobId={jobId} />}
|
|
5767
|
+
/>
|
|
5768
|
+
);
|
|
4188
5769
|
}
|
|
4189
5770
|
if (action === "delete" && rest[0]) {
|
|
4190
5771
|
return (
|
|
4191
|
-
<
|
|
4192
|
-
|
|
4193
|
-
|
|
5772
|
+
<WithResolvedJobId
|
|
5773
|
+
rawId={rest[0]}
|
|
5774
|
+
render={(jobId) => (
|
|
5775
|
+
<ApiCommand
|
|
5776
|
+
action={() => api.deleteJob(jobId)}
|
|
5777
|
+
successMessage={`Training job ${jobId} deleted`}
|
|
5778
|
+
/>
|
|
5779
|
+
)}
|
|
4194
5780
|
/>
|
|
4195
5781
|
);
|
|
4196
5782
|
}
|
|
4197
5783
|
if (action === "create") {
|
|
4198
|
-
const modelName = flags["model-name"];
|
|
4199
|
-
const datasetStrings = flags["dataset-ids"]?.split(",").filter(Boolean) ?? [];
|
|
4200
|
-
const baseModel = flags["base-model"];
|
|
4201
|
-
const epochs = flags["epochs"] ? parseInt(flags["epochs"], 10) : undefined;
|
|
4202
|
-
|
|
4203
|
-
if (!modelName || datasetStrings.length === 0) {
|
|
4204
|
-
return <ErrorMessage error="--model-name and --dataset-ids are required" />;
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
// Parse dataset strings with explicit local:/remote: prefixes
|
|
4208
|
-
const datasets: ParsedDataset[] = [];
|
|
4209
|
-
for (const ds of datasetStrings) {
|
|
4210
|
-
const parsed = parseDatasetString(ds);
|
|
4211
|
-
if ("error" in parsed) {
|
|
4212
|
-
return <ErrorMessage error={parsed.error} />;
|
|
4213
|
-
}
|
|
4214
|
-
datasets.push(parsed);
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
5784
|
return (
|
|
4218
|
-
<
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
5785
|
+
<ErrorMessage
|
|
5786
|
+
error={
|
|
5787
|
+
"`pioneer job create` has been removed.\n" +
|
|
5788
|
+
"Use `pioneer agent` to create training jobs conversationally — it will help you pick a base model,\n" +
|
|
5789
|
+
"select datasets, and configure training without needing to remember flags or dataset IDs."
|
|
5790
|
+
}
|
|
4223
5791
|
/>
|
|
4224
5792
|
);
|
|
4225
5793
|
}
|
|
@@ -4233,7 +5801,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4233
5801
|
}
|
|
4234
5802
|
|
|
4235
5803
|
if (action === "base-models" || action === "models" || action === "list") {
|
|
4236
|
-
|
|
5804
|
+
if (flags.json === "true") {
|
|
5805
|
+
return <ApiCommand action={api.listBaseModels} />;
|
|
5806
|
+
}
|
|
5807
|
+
return <BaseModelsListCommand />;
|
|
4237
5808
|
}
|
|
4238
5809
|
|
|
4239
5810
|
if (action === "encoder") {
|
|
@@ -4431,6 +6002,13 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4431
6002
|
return <Help context="model" />;
|
|
4432
6003
|
}
|
|
4433
6004
|
|
|
6005
|
+
if (normalizedAction === "base-models" || normalizedAction === "models") {
|
|
6006
|
+
if (flags.json === "true") {
|
|
6007
|
+
return <ApiCommand action={api.listBaseModels} />;
|
|
6008
|
+
}
|
|
6009
|
+
return <BaseModelsListCommand />;
|
|
6010
|
+
}
|
|
6011
|
+
|
|
4434
6012
|
if (normalizedAction === "endpoints") {
|
|
4435
6013
|
const endpointAction = rest[0];
|
|
4436
6014
|
const endpointArgs = rest.slice(1);
|
|
@@ -4507,7 +6085,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4507
6085
|
if (!modelId) {
|
|
4508
6086
|
return <ErrorMessage error="Model ID required: model endpoints get <model-id>" />;
|
|
4509
6087
|
}
|
|
4510
|
-
|
|
6088
|
+
if (flags.json === "true") {
|
|
6089
|
+
return <ApiCommand action={() => api.getProject(modelId)} />;
|
|
6090
|
+
}
|
|
6091
|
+
return <ModelEndpointGetCommand modelId={modelId} />;
|
|
4511
6092
|
}
|
|
4512
6093
|
|
|
4513
6094
|
if (endpointAction === "update") {
|
|
@@ -4558,11 +6139,14 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4558
6139
|
}
|
|
4559
6140
|
|
|
4560
6141
|
if (endpointAction === "dataset-count" || endpointAction === "count") {
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
6142
|
+
return (
|
|
6143
|
+
<ErrorMessage
|
|
6144
|
+
error={
|
|
6145
|
+
"`pioneer model endpoints dataset-count` has been removed.\n" +
|
|
6146
|
+
"The dataset count and `can_delete` flag are now surfaced inline in `pioneer model endpoints get <model-id>`."
|
|
6147
|
+
}
|
|
6148
|
+
/>
|
|
6149
|
+
);
|
|
4566
6150
|
}
|
|
4567
6151
|
|
|
4568
6152
|
if (endpointAction === "quality-metrics" || endpointAction === "quality") {
|
|
@@ -4570,40 +6154,75 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4570
6154
|
if (!modelId) {
|
|
4571
6155
|
return <ErrorMessage error="Model ID required: model endpoints quality-metrics <model-id>" />;
|
|
4572
6156
|
}
|
|
4573
|
-
|
|
6157
|
+
if (flags.json === "true") {
|
|
6158
|
+
return <ApiCommand action={() => api.getProjectQualityMetrics(modelId)} />;
|
|
6159
|
+
}
|
|
6160
|
+
return <ModelQualityMetricsCommand modelId={modelId} />;
|
|
4574
6161
|
}
|
|
4575
6162
|
|
|
4576
6163
|
if (endpointAction === "deploy") {
|
|
4577
6164
|
const modelId = endpointArgs[0];
|
|
4578
|
-
const
|
|
6165
|
+
const rawJobId = flags["job"];
|
|
4579
6166
|
|
|
4580
6167
|
if (!modelId) {
|
|
4581
|
-
return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
|
|
4582
|
-
}
|
|
4583
|
-
if (!jobId) {
|
|
4584
|
-
return <ErrorMessage error="Training job ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
|
|
4585
|
-
}
|
|
4586
|
-
if (jobId.length !== 36) {
|
|
4587
|
-
return (
|
|
4588
|
-
<Box flexDirection="column">
|
|
4589
|
-
<ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
|
|
4590
|
-
<Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
|
|
4591
|
-
<Text dimColor> Tip: Use 'pioneer model artifacts list' and 'pioneer model artifacts trained' to see full job IDs</Text>
|
|
4592
|
-
</Box>
|
|
4593
|
-
);
|
|
6168
|
+
return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> [--job <training-job-id>]" />;
|
|
4594
6169
|
}
|
|
4595
6170
|
|
|
4596
6171
|
const reason = flags["reason"];
|
|
4597
6172
|
|
|
6173
|
+
if (!rawJobId) {
|
|
6174
|
+
if (!isRawModeSupported) {
|
|
6175
|
+
return (
|
|
6176
|
+
<Box flexDirection="column">
|
|
6177
|
+
<ErrorMessage error="Interactive job picker requires a TTY." />
|
|
6178
|
+
<Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
|
|
6179
|
+
<Text dimColor> pioneer model endpoints deploy {modelId} --job <training-job-id></Text>
|
|
6180
|
+
</Box>
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
const showAll = flags["all"] === "true";
|
|
6184
|
+
return <DeployJobPickerCommand modelId={modelId} reason={reason} showAll={showAll} />;
|
|
6185
|
+
}
|
|
6186
|
+
|
|
4598
6187
|
return (
|
|
4599
|
-
<
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
6188
|
+
<WithResolvedJobId
|
|
6189
|
+
rawId={rawJobId}
|
|
6190
|
+
render={(jobId) => (
|
|
6191
|
+
<ApiCommand
|
|
6192
|
+
action={async () => {
|
|
6193
|
+
// Deploy requires the job's project_id to match the target.
|
|
6194
|
+
// PATCH first to link them; the backend treats a no-op assign
|
|
6195
|
+
// as success. While we have the job around, capture its
|
|
6196
|
+
// base_model so we can enrich the deployment record (which
|
|
6197
|
+
// currently returns base_model=null for training-job deploys).
|
|
6198
|
+
const jobInfo = await api.getJob(jobId);
|
|
6199
|
+
const patch = await api.updateTrainingJob(jobId, {
|
|
6200
|
+
project_id: modelId,
|
|
6201
|
+
});
|
|
6202
|
+
if (!patch.ok) {
|
|
6203
|
+
return patch as api.ApiResult<unknown>;
|
|
6204
|
+
}
|
|
6205
|
+
const result = await api.deployTrainingJobToProject(modelId, {
|
|
6206
|
+
training_job_id: jobId,
|
|
6207
|
+
...(reason ? { reason } : {}),
|
|
6208
|
+
});
|
|
6209
|
+
if (
|
|
6210
|
+
result.ok &&
|
|
6211
|
+
result.data &&
|
|
6212
|
+
jobInfo.ok &&
|
|
6213
|
+
jobInfo.data?.base_model &&
|
|
6214
|
+
!result.data.base_model
|
|
6215
|
+
) {
|
|
6216
|
+
return {
|
|
6217
|
+
...result,
|
|
6218
|
+
data: { ...result.data, base_model: jobInfo.data.base_model },
|
|
6219
|
+
};
|
|
6220
|
+
}
|
|
6221
|
+
return result;
|
|
6222
|
+
}}
|
|
6223
|
+
successMessage={`Deployment initiated for project ${modelId} from job ${jobId}`}
|
|
6224
|
+
/>
|
|
6225
|
+
)}
|
|
4607
6226
|
/>
|
|
4608
6227
|
);
|
|
4609
6228
|
}
|
|
@@ -4664,37 +6283,33 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4664
6283
|
if (!artifactArgs[0]) {
|
|
4665
6284
|
return <ErrorMessage error="Job ID required: model artifacts download <job-id>" />;
|
|
4666
6285
|
}
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
6286
|
+
return (
|
|
6287
|
+
<WithResolvedJobId
|
|
6288
|
+
rawId={artifactArgs[0]}
|
|
6289
|
+
render={(jobId) =>
|
|
6290
|
+
flags.json === "true" ? (
|
|
6291
|
+
<ApiCommand action={() => api.downloadModel(jobId)} />
|
|
6292
|
+
) : (
|
|
6293
|
+
<ModelArtifactDownloadCommand jobId={jobId} />
|
|
6294
|
+
)
|
|
6295
|
+
}
|
|
6296
|
+
/>
|
|
6297
|
+
);
|
|
4678
6298
|
}
|
|
4679
6299
|
|
|
4680
6300
|
if (artifactsAction === "delete") {
|
|
4681
6301
|
if (!artifactArgs[0]) {
|
|
4682
6302
|
return <ErrorMessage error="Model ID required: model artifacts delete <job-id>" />;
|
|
4683
6303
|
}
|
|
4684
|
-
const jobId = artifactArgs[0];
|
|
4685
|
-
if (jobId.length !== 36) {
|
|
4686
|
-
return (
|
|
4687
|
-
<Box flexDirection="column">
|
|
4688
|
-
<ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
|
|
4689
|
-
<Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
|
|
4690
|
-
<Text dimColor> Tip: Use 'pioneer model artifacts list' to see full job IDs</Text>
|
|
4691
|
-
</Box>
|
|
4692
|
-
);
|
|
4693
|
-
}
|
|
4694
6304
|
return (
|
|
4695
|
-
<
|
|
4696
|
-
|
|
4697
|
-
|
|
6305
|
+
<WithResolvedJobId
|
|
6306
|
+
rawId={artifactArgs[0]}
|
|
6307
|
+
render={(jobId) => (
|
|
6308
|
+
<ApiCommand
|
|
6309
|
+
action={() => api.deleteModel(jobId)}
|
|
6310
|
+
successMessage={`Model ${jobId} deleted`}
|
|
6311
|
+
/>
|
|
6312
|
+
)}
|
|
4698
6313
|
/>
|
|
4699
6314
|
);
|
|
4700
6315
|
}
|
|
@@ -4720,7 +6335,7 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4720
6335
|
if (!artifactArgs[0]) {
|
|
4721
6336
|
return <ErrorMessage error="Job ID required: model artifacts upload <job-id> --to hf --repo <repo>" />;
|
|
4722
6337
|
}
|
|
4723
|
-
const
|
|
6338
|
+
const rawJobId = artifactArgs[0];
|
|
4724
6339
|
const repo = flags["repo"];
|
|
4725
6340
|
const hfTokenFlag = flags["hf-token"];
|
|
4726
6341
|
const isPrivate = flags["private"]?.toLowerCase() === "true";
|
|
@@ -4744,15 +6359,20 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4744
6359
|
}
|
|
4745
6360
|
|
|
4746
6361
|
return (
|
|
4747
|
-
<
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
6362
|
+
<WithResolvedJobId
|
|
6363
|
+
rawId={rawJobId}
|
|
6364
|
+
render={(jobId) => (
|
|
6365
|
+
<ApiCommand
|
|
6366
|
+
action={() =>
|
|
6367
|
+
api.pushModelToHub(jobId, {
|
|
6368
|
+
hf_token: hfToken,
|
|
6369
|
+
repo_id: repo,
|
|
6370
|
+
private: isPrivate,
|
|
6371
|
+
})
|
|
6372
|
+
}
|
|
6373
|
+
successMessage={`Model uploaded to Hugging Face: ${repo}`}
|
|
6374
|
+
/>
|
|
6375
|
+
)}
|
|
4756
6376
|
/>
|
|
4757
6377
|
);
|
|
4758
6378
|
}
|
|
@@ -4951,13 +6571,15 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4951
6571
|
return <Help context="agent" />;
|
|
4952
6572
|
}
|
|
4953
6573
|
|
|
4954
|
-
if (action && !action.startsWith("-")) {
|
|
6574
|
+
if (action && action !== "resume" && action !== "sessions" && !action.startsWith("-")) {
|
|
4955
6575
|
return (
|
|
4956
6576
|
<ErrorMessage
|
|
4957
6577
|
error={
|
|
4958
6578
|
'Invalid agent command syntax. Use one of:\n' +
|
|
4959
6579
|
"pioneer agent\n" +
|
|
4960
|
-
"pioneer agent --mode research"
|
|
6580
|
+
"pioneer agent --mode research\n" +
|
|
6581
|
+
"pioneer agent sessions\n" +
|
|
6582
|
+
"pioneer agent resume [conversation-id]"
|
|
4961
6583
|
}
|
|
4962
6584
|
/>
|
|
4963
6585
|
);
|
|
@@ -4992,11 +6614,34 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
|
|
|
4992
6614
|
return <ErrorMessage error='--filters is not supported for /auto-agent/clarify. Omit this flag for now.' />;
|
|
4993
6615
|
}
|
|
4994
6616
|
|
|
6617
|
+
if (action === "resume" || action === "sessions") {
|
|
6618
|
+
if (!isRawModeSupported) {
|
|
6619
|
+
return (
|
|
6620
|
+
<ErrorMessage
|
|
6621
|
+
error="Interactive input is not supported in this terminal.\nUse interactive mode for this environment: agent --help"
|
|
6622
|
+
/>
|
|
6623
|
+
);
|
|
6624
|
+
}
|
|
6625
|
+
if (rest[0] || flags["conversation-id"]) {
|
|
6626
|
+
const resumeId = rest[0] ?? flags["conversation-id"];
|
|
6627
|
+
return (
|
|
6628
|
+
<AutoAgentInteractiveSession
|
|
6629
|
+
conversationId={resumeId}
|
|
6630
|
+
history={history}
|
|
6631
|
+
mode={mode}
|
|
6632
|
+
allowSessionCreation={false}
|
|
6633
|
+
/>
|
|
6634
|
+
);
|
|
6635
|
+
}
|
|
6636
|
+
return <AgentResumeCommand mode={mode} />;
|
|
6637
|
+
}
|
|
6638
|
+
|
|
4995
6639
|
return (
|
|
4996
6640
|
<AgentInteractivePrompt
|
|
4997
6641
|
conversationId={flags["conversation-id"]}
|
|
4998
6642
|
history={history}
|
|
4999
6643
|
mode={mode}
|
|
6644
|
+
allowSessionCreation={true}
|
|
5000
6645
|
/>
|
|
5001
6646
|
);
|
|
5002
6647
|
}
|
|
@@ -5027,4 +6672,6 @@ async function main() {
|
|
|
5027
6672
|
await render(<App command={command} flags={flags} parseErrors={parseErrors} />).waitUntilExit();
|
|
5028
6673
|
}
|
|
5029
6674
|
|
|
5030
|
-
|
|
6675
|
+
if (process.env.PIONEER_SKIP_AUTORUN !== "true") {
|
|
6676
|
+
main();
|
|
6677
|
+
}
|