qaos 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +23 -11
- package/dist/commands/auth.js +4 -5
- package/dist/commands/hist.js +2 -2
- package/dist/components/run/run-ui.js +70 -92
- package/dist/entry-functions.js +19 -5
- package/dist/i18n/locales/en.json +3 -0
- package/dist/i18n/locales/fr.json +3 -0
- package/dist/prod-config.d.ts +1 -1
- package/dist/prod-config.js +1 -1
- package/dist/services/auth-service.d.ts +2 -2
- package/dist/services/auth-service.js +2 -2
- package/dist/types/next-actions-response.d.ts +4 -14
- package/dist/types/run-ui-state.d.ts +2 -6
- package/dist/utils/config-validator.js +38 -2
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -60,17 +60,28 @@ export default function App(args) {
|
|
|
60
60
|
phase: 'completed',
|
|
61
61
|
};
|
|
62
62
|
case 'server_state': {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
// Prefer the authoritative task list from the server (includes subtasks
|
|
64
|
+
// with metadata). Fall back to merging taskStatus into existing tasks
|
|
65
|
+
// for older server versions that don't send taskList.
|
|
66
|
+
let updatedTasks;
|
|
67
|
+
if (event.taskList && event.taskList.length > 0) {
|
|
68
|
+
updatedTasks = event.taskList;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const taskStatusMap = event.taskStatus ?? {};
|
|
72
|
+
updatedTasks =
|
|
73
|
+
baseState.tasks.length > 0
|
|
74
|
+
? baseState.tasks.map(task => ({
|
|
75
|
+
...task,
|
|
76
|
+
status: taskStatusMap[task.id] ?? task.status,
|
|
77
|
+
}))
|
|
78
|
+
: Object.keys(taskStatusMap).map(taskId => ({
|
|
79
|
+
id: taskId,
|
|
80
|
+
description: `Task ${taskId}`,
|
|
81
|
+
status: taskStatusMap[taskId],
|
|
82
|
+
isSubtask: false,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
74
85
|
return {
|
|
75
86
|
...baseState,
|
|
76
87
|
thinking: event.thinking,
|
|
@@ -210,6 +221,7 @@ export default function App(args) {
|
|
|
210
221
|
if (!runUiState)
|
|
211
222
|
return React.createElement(Text, null, t('run.preparing'));
|
|
212
223
|
if (runUiState.phase === 'error') {
|
|
224
|
+
setTimeout(() => process.exit(1), EXIT_DELAY_ERROR);
|
|
213
225
|
return (React.createElement(RunError, { message: runUiState.errorMessage || t('auth.unknownError') }));
|
|
214
226
|
}
|
|
215
227
|
if (runUiState.phase === 'completed') {
|
package/dist/commands/auth.js
CHANGED
|
@@ -66,7 +66,6 @@ export async function authenticate(args, t) {
|
|
|
66
66
|
try {
|
|
67
67
|
const method = args.method || 'ui ';
|
|
68
68
|
const config = await configPromise;
|
|
69
|
-
const serverUrl = config.API_SERVER_URL;
|
|
70
69
|
const websiteUrl = config.WEBSITE_URL;
|
|
71
70
|
if (method === 'api') {
|
|
72
71
|
if (!args.token) {
|
|
@@ -76,7 +75,7 @@ export async function authenticate(args, t) {
|
|
|
76
75
|
error: t('auth.usage'),
|
|
77
76
|
};
|
|
78
77
|
}
|
|
79
|
-
const validationResult = await authService.validateToken(args.token,
|
|
78
|
+
const validationResult = await authService.validateToken(args.token, websiteUrl);
|
|
80
79
|
if (!validationResult.valid) {
|
|
81
80
|
return {
|
|
82
81
|
success: false,
|
|
@@ -84,7 +83,7 @@ export async function authenticate(args, t) {
|
|
|
84
83
|
error: validationResult.error || t('auth.invalidToken'),
|
|
85
84
|
};
|
|
86
85
|
}
|
|
87
|
-
await authService.saveToken(args.token,
|
|
86
|
+
await authService.saveToken(args.token, websiteUrl, validationResult.userId);
|
|
88
87
|
return {
|
|
89
88
|
success: true,
|
|
90
89
|
message: t('auth.successWithPath', {
|
|
@@ -111,10 +110,10 @@ export async function authenticate(args, t) {
|
|
|
111
110
|
*/
|
|
112
111
|
export async function getAuthStatus() {
|
|
113
112
|
const isAuthenticated = await authService.isAuthenticated();
|
|
114
|
-
const
|
|
113
|
+
const websiteUrl = await authService.getWebsiteUrl();
|
|
115
114
|
return {
|
|
116
115
|
authenticated: isAuthenticated,
|
|
117
|
-
serverUrl:
|
|
116
|
+
serverUrl: websiteUrl || undefined,
|
|
118
117
|
};
|
|
119
118
|
}
|
|
120
119
|
/**
|
package/dist/commands/hist.js
CHANGED
|
@@ -5,8 +5,8 @@ export async function fetchRuns(options) {
|
|
|
5
5
|
if (!token) {
|
|
6
6
|
return { success: false, error: 'not_authenticated' };
|
|
7
7
|
}
|
|
8
|
-
const defaultServerUrl = (await configPromise).
|
|
9
|
-
const serverUrl = (await authService.
|
|
8
|
+
const defaultServerUrl = (await configPromise).WEBSITE_URL;
|
|
9
|
+
const serverUrl = (await authService.getWebsiteUrl()) || defaultServerUrl;
|
|
10
10
|
const url = new URL('/api/cli/runs', serverUrl);
|
|
11
11
|
if (options.limit) {
|
|
12
12
|
url.searchParams.set('limit', String(options.limit));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { Box, Text, measureElement } from 'ink';
|
|
3
|
-
import { Badge
|
|
3
|
+
import { Badge } from '@inkjs/ui';
|
|
4
4
|
import Spinner from './spinner.js';
|
|
5
5
|
import ThinkingAnimation from './thinking-animation.js';
|
|
6
6
|
import { lightBlue, purple } from '../../constants/colors.js';
|
|
@@ -48,22 +48,22 @@ function AnimatedProgressBar({ value, isActive, total, }) {
|
|
|
48
48
|
React.createElement(Text, { color: "magenta" }, SCAN_CHAR))) : (React.createElement(Text, { key: i, dimColor: true }, segment))))),
|
|
49
49
|
emptyAfter > 0 && (React.createElement(Text, { dimColor: true }, REMAINING_CHAR.repeat(emptyAfter)))));
|
|
50
50
|
}
|
|
51
|
-
function renderTask(task, actions, isActive) {
|
|
51
|
+
function renderTask(task, actions, isActive, duration) {
|
|
52
52
|
const color = STATUS_COLORS[task.status] || 'red';
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const indent = task.isSubtask ? 2 : 0;
|
|
54
|
+
return (React.createElement(Box, { key: task.id, flexDirection: "column", marginLeft: indent },
|
|
55
|
+
task.status === 'doing' && (React.createElement(Box, { gap: 1 },
|
|
55
56
|
React.createElement(Spinner, { color: color }),
|
|
56
|
-
React.createElement(Text, { color: color, bold: true },
|
|
57
|
-
|
|
58
|
-
task.description))),
|
|
57
|
+
React.createElement(Text, { color: color, bold: true }, task.description),
|
|
58
|
+
duration !== undefined && (React.createElement(Text, { dimColor: true }, duration)))),
|
|
59
59
|
task.status === 'notStarted' && (React.createElement(Text, null,
|
|
60
60
|
React.createElement(Text, { color: color }, "\u25CB"),
|
|
61
61
|
' ',
|
|
62
62
|
React.createElement(Text, { color: color, dimColor: true }, task.description))),
|
|
63
63
|
task.status === 'done' && (React.createElement(Text, null,
|
|
64
|
-
React.createElement(Text, { color:
|
|
64
|
+
React.createElement(Text, { color: "green" }, "\u2713"),
|
|
65
65
|
' ',
|
|
66
|
-
React.createElement(Text, { color:
|
|
66
|
+
React.createElement(Text, { color: "green" }, task.description))),
|
|
67
67
|
task.status === 'doing' && isActive && actions.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, actions.map((action, index) => (React.createElement(Text, { key: `${action}-${index}`, dimColor: true },
|
|
68
68
|
"\u2514\u2500 ",
|
|
69
69
|
action)))))));
|
|
@@ -102,49 +102,33 @@ function formatDuration(seconds) {
|
|
|
102
102
|
const s = seconds % 60;
|
|
103
103
|
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const indent = task.isSubtask ? 2 : 0;
|
|
108
|
-
return (React.createElement(Box, { key: task.id, flexDirection: "column", marginLeft: indent },
|
|
109
|
-
task.status === 'doing' && (React.createElement(Box, { gap: 1 },
|
|
110
|
-
React.createElement(Spinner, { color: color }),
|
|
111
|
-
React.createElement(Text, { color: color, bold: true }, task.description),
|
|
112
|
-
duration !== undefined && (React.createElement(Text, { dimColor: true }, duration)))),
|
|
113
|
-
task.status === 'notStarted' && (React.createElement(Text, null,
|
|
114
|
-
React.createElement(Text, { color: color }, "\u25CB"),
|
|
115
|
-
' ',
|
|
116
|
-
React.createElement(Text, { color: color, dimColor: true }, task.description))),
|
|
117
|
-
task.status === 'done' && (React.createElement(Text, null,
|
|
118
|
-
React.createElement(Text, { color: "green" }, "\u2713"),
|
|
119
|
-
' ',
|
|
120
|
-
React.createElement(Text, { color: "green" }, task.description))),
|
|
121
|
-
task.status === 'doing' && isActive && actions.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, actions.map((action, index) => (React.createElement(Text, { key: `${action}-${index}`, dimColor: true },
|
|
122
|
-
"\u2514\u2500 ",
|
|
123
|
-
action)))))));
|
|
124
|
-
}
|
|
125
|
-
function QaosProgressSection({ qaos, actions }) {
|
|
105
|
+
/** Shared task list display: progress bars (top-level + subtasks), duration tracking, windowed list. */
|
|
106
|
+
function TaskSection({ tasks, actions }) {
|
|
126
107
|
const { t } = useLanguage();
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
//
|
|
108
|
+
const topLevelTasks = tasks.filter(t => !t.isSubtask);
|
|
109
|
+
const tasksDone = topLevelTasks.filter(t => t.status === 'done').length;
|
|
110
|
+
const taskProgress = topLevelTasks.length > 0 ? tasksDone / topLevelTasks.length : 0;
|
|
111
|
+
// When a subtask is running, its parent is already 'done' (subtasks run after the
|
|
112
|
+
// parent completes). Find the active subtask first, then resolve its parent.
|
|
113
|
+
const activeSubtask = tasks.find(t => t.isSubtask && t.status === 'doing');
|
|
114
|
+
const activeTopLevelTask = activeSubtask
|
|
115
|
+
? topLevelTasks.find(t => t.id === activeSubtask.parentTaskId)
|
|
116
|
+
: topLevelTasks.find(t => t.status === 'doing');
|
|
117
|
+
const activeTaskSubtasks = activeTopLevelTask
|
|
118
|
+
? tasks.filter(t => t.isSubtask && t.parentTaskId === activeTopLevelTask.id)
|
|
119
|
+
: [];
|
|
120
|
+
const subtasksDone = activeTaskSubtasks.filter(t => t.status === 'done').length;
|
|
121
|
+
const subtaskProgress = activeTaskSubtasks.length > 0 ? subtasksDone / activeTaskSubtasks.length : 0;
|
|
122
|
+
// The active task for duration tracking is whichever task is currently 'doing'
|
|
123
|
+
const activeTask = tasks.find(t => t.status === 'doing');
|
|
124
|
+
// Duration tracking
|
|
131
125
|
const [now, setNow] = useState(() => Date.now());
|
|
132
|
-
const pageStartRef = useRef(null);
|
|
133
126
|
const taskStartRef = useRef(null);
|
|
134
|
-
const prevPageUrlRef = useRef(null);
|
|
135
127
|
const prevTaskIdRef = useRef(null);
|
|
136
128
|
useEffect(() => {
|
|
137
129
|
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
138
130
|
return () => clearInterval(timer);
|
|
139
131
|
}, []);
|
|
140
|
-
// Reset page start time when the current page URL changes
|
|
141
|
-
useEffect(() => {
|
|
142
|
-
if (qaos.currentPageUrl !== prevPageUrlRef.current) {
|
|
143
|
-
prevPageUrlRef.current = qaos.currentPageUrl;
|
|
144
|
-
pageStartRef.current = qaos.currentPageUrl !== null ? Date.now() : null;
|
|
145
|
-
}
|
|
146
|
-
}, [qaos.currentPageUrl]);
|
|
147
|
-
// Reset task start time when the active task changes
|
|
148
132
|
const activeTaskId = activeTask?.id ?? null;
|
|
149
133
|
useEffect(() => {
|
|
150
134
|
if (activeTaskId !== prevTaskIdRef.current) {
|
|
@@ -152,32 +136,14 @@ function QaosProgressSection({ qaos, actions }) {
|
|
|
152
136
|
taskStartRef.current = activeTaskId !== null ? Date.now() : null;
|
|
153
137
|
}
|
|
154
138
|
}, [activeTaskId]);
|
|
155
|
-
const pageDuration = pageStartRef.current !== null
|
|
156
|
-
? formatDuration(Math.max(0, Math.floor((now - pageStartRef.current) / 1000)))
|
|
157
|
-
: undefined;
|
|
158
139
|
const taskDuration = taskStartRef.current !== null
|
|
159
140
|
? formatDuration(Math.max(0, Math.floor((now - taskStartRef.current) / 1000)))
|
|
160
141
|
: undefined;
|
|
161
|
-
// QAOS Explore tasks are represented by the "Exploring X …" line — exclude
|
|
162
|
-
// them from the task list and progress bars.
|
|
163
|
-
const displayTasks = qaos.tasks.filter(t => !t.description.includes('[QAOS Explore]'));
|
|
164
|
-
// Global tasks progress (top-level, exploration excluded)
|
|
165
|
-
const topLevelTasks = displayTasks.filter(t => !t.isSubtask);
|
|
166
|
-
const tasksDone = topLevelTasks.filter(t => t.status === 'done').length;
|
|
167
|
-
const taskProgress = topLevelTasks.length > 0 ? tasksDone / topLevelTasks.length : 0;
|
|
168
|
-
// Subtask progress scoped to the currently active top-level task
|
|
169
|
-
const activeTopLevelTask = topLevelTasks.find(t => t.status === 'doing');
|
|
170
|
-
const activeTaskSubtasks = activeTopLevelTask
|
|
171
|
-
? displayTasks.filter(t => t.isSubtask && t.parentTaskId === activeTopLevelTask.id)
|
|
172
|
-
: [];
|
|
173
|
-
const subtasksDone = activeTaskSubtasks.filter(t => t.status === 'done').length;
|
|
174
|
-
const subtaskProgress = activeTaskSubtasks.length > 0 ? subtasksDone / activeTaskSubtasks.length : 0;
|
|
175
142
|
// Re-sort into chronological display order: done → doing → notStarted
|
|
176
|
-
// (server sends them as doing → notStarted → done, which is the wrong visual order)
|
|
177
143
|
const sortedTasks = [
|
|
178
|
-
...
|
|
179
|
-
...
|
|
180
|
-
...
|
|
144
|
+
...tasks.filter(t => t.status === 'done'),
|
|
145
|
+
...tasks.filter(t => t.status === 'doing'),
|
|
146
|
+
...tasks.filter(t => t.status === 'notStarted'),
|
|
181
147
|
];
|
|
182
148
|
// Windowed task list: show at most 11 tasks centered on the active one
|
|
183
149
|
const WINDOW_HALF = 5;
|
|
@@ -197,21 +163,11 @@ function QaosProgressSection({ qaos, actions }) {
|
|
|
197
163
|
hiddenAfter = sortedTasks.length - endIdx;
|
|
198
164
|
visibleTasks = sortedTasks.slice(startIdx, endIdx);
|
|
199
165
|
}
|
|
200
|
-
return (React.createElement(
|
|
201
|
-
React.createElement(Text, { color: purple, bold: true }, t('run.pageExploration')),
|
|
202
|
-
currentPage && (React.createElement(Box, { gap: 1, marginTop: 1 },
|
|
203
|
-
React.createElement(Spinner, { color: lightBlue }),
|
|
204
|
-
React.createElement(Text, { color: lightBlue },
|
|
205
|
-
' ',
|
|
206
|
-
t('run.exploring'),
|
|
207
|
-
" ",
|
|
208
|
-
React.createElement(Text, { bold: true }, currentPage),
|
|
209
|
-
" ..."),
|
|
210
|
-
pageDuration !== undefined && (React.createElement(Text, { dimColor: true }, pageDuration)))),
|
|
166
|
+
return (React.createElement(React.Fragment, null,
|
|
211
167
|
(topLevelTasks.length > 0 || activeTaskSubtasks.length > 0) && (React.createElement(Box, { flexDirection: "column", marginY: 1, gap: 0 },
|
|
212
168
|
topLevelTasks.length > 0 && (React.createElement(Box, { gap: 1 },
|
|
213
169
|
React.createElement(Text, { dimColor: true }, t('run.progressTasks')),
|
|
214
|
-
React.createElement(AnimatedProgressBar, { value: taskProgress * 100, isActive:
|
|
170
|
+
React.createElement(AnimatedProgressBar, { value: taskProgress * 100, isActive: activeTopLevelTask !== undefined, total: topLevelTasks.length }),
|
|
215
171
|
React.createElement(Text, { dimColor: true },
|
|
216
172
|
tasksDone,
|
|
217
173
|
"/",
|
|
@@ -223,7 +179,7 @@ function QaosProgressSection({ qaos, actions }) {
|
|
|
223
179
|
subtasksDone,
|
|
224
180
|
"/",
|
|
225
181
|
activeTaskSubtasks.length))))),
|
|
226
|
-
|
|
182
|
+
sortedTasks.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
227
183
|
hiddenBefore > 0 && (React.createElement(Text, { dimColor: true },
|
|
228
184
|
' ',
|
|
229
185
|
"\u2191 ",
|
|
@@ -231,7 +187,7 @@ function QaosProgressSection({ qaos, actions }) {
|
|
|
231
187
|
" more task",
|
|
232
188
|
hiddenBefore > 1 ? 's' : '',
|
|
233
189
|
" above")),
|
|
234
|
-
visibleTasks.map(task =>
|
|
190
|
+
visibleTasks.map(task => renderTask(task, actions, task === activeTask, task === activeTask ? taskDuration : undefined)),
|
|
235
191
|
hiddenAfter > 0 && (React.createElement(Text, { dimColor: true },
|
|
236
192
|
' ',
|
|
237
193
|
"\u2193 ",
|
|
@@ -240,6 +196,40 @@ function QaosProgressSection({ qaos, actions }) {
|
|
|
240
196
|
hiddenAfter > 1 ? 's' : '',
|
|
241
197
|
" below"))))));
|
|
242
198
|
}
|
|
199
|
+
function QaosProgressSection({ qaos, actions }) {
|
|
200
|
+
const { t } = useLanguage();
|
|
201
|
+
const shortenedUrl = qaos.currentPageUrl ? shortenUrl(qaos.currentPageUrl) : null;
|
|
202
|
+
const currentPage = shortenedUrl && shortenedUrl.trim() ? shortenedUrl : null;
|
|
203
|
+
// Page-level duration tracking
|
|
204
|
+
const [now, setNow] = useState(() => Date.now());
|
|
205
|
+
const pageStartRef = useRef(null);
|
|
206
|
+
const prevPageUrlRef = useRef(null);
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
209
|
+
return () => clearInterval(timer);
|
|
210
|
+
}, []);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (qaos.currentPageUrl !== prevPageUrlRef.current) {
|
|
213
|
+
prevPageUrlRef.current = qaos.currentPageUrl;
|
|
214
|
+
pageStartRef.current = qaos.currentPageUrl !== null ? Date.now() : null;
|
|
215
|
+
}
|
|
216
|
+
}, [qaos.currentPageUrl]);
|
|
217
|
+
const pageDuration = pageStartRef.current !== null
|
|
218
|
+
? formatDuration(Math.max(0, Math.floor((now - pageStartRef.current) / 1000)))
|
|
219
|
+
: undefined;
|
|
220
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
221
|
+
React.createElement(Text, { color: purple, bold: true }, t('run.pageExploration')),
|
|
222
|
+
currentPage && (React.createElement(Box, { gap: 1, marginTop: 1 },
|
|
223
|
+
React.createElement(Spinner, { color: lightBlue }),
|
|
224
|
+
React.createElement(Text, { color: lightBlue },
|
|
225
|
+
' ',
|
|
226
|
+
t('run.exploring'),
|
|
227
|
+
" ",
|
|
228
|
+
React.createElement(Text, { bold: true }, currentPage),
|
|
229
|
+
" ..."),
|
|
230
|
+
pageDuration !== undefined && (React.createElement(Text, { dimColor: true }, pageDuration)))),
|
|
231
|
+
React.createElement(TaskSection, { tasks: qaos.tasks, actions: actions })));
|
|
232
|
+
}
|
|
243
233
|
export function RunConnecting({ uiState }) {
|
|
244
234
|
const { t } = useLanguage();
|
|
245
235
|
const badge = getConnectionBadge(uiState.connection, t);
|
|
@@ -266,14 +256,8 @@ export default function RunUi({ uiState }) {
|
|
|
266
256
|
const badge = getConnectionBadge(uiState.connection, t);
|
|
267
257
|
const thinkingText = uiState.thinking || 'Waiting for agent state...';
|
|
268
258
|
const reportUrl = uiState.reportUrl;
|
|
269
|
-
// Find the currently active (doing) task
|
|
270
|
-
const activeTask = uiState.tasks.find(task => task.status === 'doing');
|
|
271
259
|
const actions = uiState.actions.length > 0 ? uiState.actions : [];
|
|
272
260
|
const isQaos = !!uiState.qaosProgress;
|
|
273
|
-
// Calculate task completion (for non-QAOS mode)
|
|
274
|
-
const completedTasks = uiState.tasks.filter(task => task.status === 'done').length;
|
|
275
|
-
const totalTasks = uiState.tasks.length;
|
|
276
|
-
const progress = totalTasks > 0 ? completedTasks / totalTasks : 0;
|
|
277
261
|
return (React.createElement(Box, { borderStyle: "single", borderColor: "white", paddingX: 1, flexDirection: "column" },
|
|
278
262
|
React.createElement(Badge, { color: badge.color }, badge.label),
|
|
279
263
|
React.createElement(Box, { paddingX: 1, marginTop: 1 },
|
|
@@ -288,11 +272,5 @@ export default function RunUi({ uiState }) {
|
|
|
288
272
|
React.createElement(Text, { color: "cyan", bold: true }, `\x1b]8;;${reportUrl}\x07${reportUrl}\x1b]8;;\x07`))),
|
|
289
273
|
React.createElement(Box, { borderStyle: "round", borderColor: purple, paddingX: 1, flexDirection: "column", marginTop: 1 }, isQaos ? (React.createElement(QaosProgressSection, { qaos: uiState.qaosProgress, actions: actions })) : (React.createElement(React.Fragment, null,
|
|
290
274
|
React.createElement(Text, { color: purple, bold: true }, t('run.tasks')),
|
|
291
|
-
React.createElement(
|
|
292
|
-
React.createElement(ProgressBar, { value: progress * 100 }),
|
|
293
|
-
React.createElement(Text, { dimColor: true },
|
|
294
|
-
completedTasks,
|
|
295
|
-
"/",
|
|
296
|
-
totalTasks)),
|
|
297
|
-
uiState.tasks.length === 0 ? (React.createElement(Text, { color: "gray" }, t('run.noTasks'))) : (uiState.tasks.map(task => renderTask(task, actions, task === activeTask))))))));
|
|
275
|
+
uiState.tasks.length === 0 ? (React.createElement(Text, { color: "gray" }, t('run.noTasks'))) : (React.createElement(TaskSection, { tasks: uiState.tasks, actions: actions })))))));
|
|
298
276
|
}
|
package/dist/entry-functions.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createSerializedDOMState, getLLMRepresentation, getInteractiveElementCo
|
|
|
3
3
|
import { browserService } from './services/browser-service.js';
|
|
4
4
|
import { BrowserActionService, } from './services/browser-action-service.js';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { stateService } from './services/state-service.js';
|
|
7
8
|
import { debug } from './utils/logger.js';
|
|
8
9
|
import { getAuthStatus } from './commands/auth.js';
|
|
@@ -10,7 +11,6 @@ import { authService } from './services/auth-service.js';
|
|
|
10
11
|
import { validateConfig } from './utils/config-validator.js';
|
|
11
12
|
import { configPromise } from './config.js';
|
|
12
13
|
const DEFAULT_TASK_DESCRIPTION = 'Task';
|
|
13
|
-
const apiUrl = (await configPromise).API_SERVER_URL;
|
|
14
14
|
function extractTasksFromConfig(configData) {
|
|
15
15
|
const tasks = Array.isArray(configData.tasks)
|
|
16
16
|
? configData.tasks
|
|
@@ -25,6 +25,7 @@ function extractTasksFromConfig(configData) {
|
|
|
25
25
|
id,
|
|
26
26
|
description,
|
|
27
27
|
status: 'notStarted',
|
|
28
|
+
isSubtask: false,
|
|
28
29
|
};
|
|
29
30
|
});
|
|
30
31
|
}
|
|
@@ -130,13 +131,25 @@ export async function run(args, t) {
|
|
|
130
131
|
args.onUiEvent?.({ type: 'error', message });
|
|
131
132
|
throw new Error(message);
|
|
132
133
|
}
|
|
134
|
+
// Assign UUIDs to any tasks missing an id
|
|
135
|
+
const cfg = rawConfigData;
|
|
136
|
+
if (Array.isArray(cfg['tasks'])) {
|
|
137
|
+
cfg['tasks'] = cfg['tasks'].map(task => ({
|
|
138
|
+
...task,
|
|
139
|
+
id: typeof task['id'] === 'string' && task['id'].trim() !== ''
|
|
140
|
+
? task['id']
|
|
141
|
+
: randomUUID(),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
133
144
|
const configData = rawConfigData;
|
|
134
145
|
const projectId = configData['projectId'];
|
|
135
|
-
const
|
|
146
|
+
const websiteUrl = (await configPromise).WEBSITE_URL;
|
|
147
|
+
const savedWebsiteUrl = await authService.getWebsiteUrl();
|
|
148
|
+
const validationBaseUrl = websiteUrl || savedWebsiteUrl;
|
|
136
149
|
const apiToken = await authService.getAccessToken();
|
|
137
|
-
if (
|
|
150
|
+
if (validationBaseUrl && apiToken) {
|
|
138
151
|
try {
|
|
139
|
-
const validateUrl = new URL(`/api/cli/projects/${encodeURIComponent(projectId)}/validate`,
|
|
152
|
+
const validateUrl = new URL(`/api/cli/projects/${encodeURIComponent(projectId)}/validate`, validationBaseUrl);
|
|
140
153
|
const response = await fetch(validateUrl.toString(), {
|
|
141
154
|
method: 'POST',
|
|
142
155
|
headers: {
|
|
@@ -189,7 +202,7 @@ export async function run(args, t) {
|
|
|
189
202
|
args.onUiEvent?.({ type: 'connection_status', status });
|
|
190
203
|
},
|
|
191
204
|
onRunStarted: data => {
|
|
192
|
-
const appUrl =
|
|
205
|
+
const appUrl = websiteUrl;
|
|
193
206
|
const projectId = configData['projectId'];
|
|
194
207
|
const reportUrl = projectId && data.runId
|
|
195
208
|
? `${appUrl}/project/${projectId}/report/${data.runId}`
|
|
@@ -201,6 +214,7 @@ export async function run(args, t) {
|
|
|
201
214
|
type: 'server_state',
|
|
202
215
|
thinking: data.state.thinking,
|
|
203
216
|
taskStatus: data.state.taskStatus,
|
|
217
|
+
taskList: data.state.taskList ?? undefined,
|
|
204
218
|
actions: data.action?.map(action => formatActionSummary(action, t)) ?? [],
|
|
205
219
|
qaosProgress: data.state.qaosProgress ?? undefined,
|
|
206
220
|
});
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"errors.configMissingProjectId": "Config is missing required field \"projectId\" (must be a non-empty string).",
|
|
117
117
|
"errors.configEnvFileMustBeString": "Config field \"envFile\" must be a string path.",
|
|
118
118
|
"errors.configQaosModeNotBoolean": "Config field \"qaosMode\" must be a boolean (true or false).",
|
|
119
|
+
"errors.configMaxBudgetMustBePositiveNumber": "Config field \"maxBudget\" must be a positive number.",
|
|
119
120
|
"errors.configQaosConfigRequired": "Config has \"qaosMode: true\" but is missing the required \"qaosConfig\" object.",
|
|
120
121
|
"errors.configQaosStartUrlRequired": "Config \"qaosConfig.startUrl\" is required and must be a non-empty string.",
|
|
121
122
|
"errors.configQaosMaxPagesMustBePositiveInt": "Config \"qaosConfig.maxPages\" must be a positive integer.",
|
|
@@ -130,6 +131,8 @@
|
|
|
130
131
|
"errors.aiDecisionParseFailed": "The AI agent failed to produce a valid response after several retries. The run has been stopped.",
|
|
131
132
|
"errors.projectNotFound": "Project \"{{projectId}}\" does not exist or you do not have access to it.",
|
|
132
133
|
"errors.projectValidationFailed": "Could not verify project access: {{error}}",
|
|
134
|
+
"errors.configWebsiteOriginRequiredForFilesystem": "\"websiteOrigin\" is required when using filesystem paths (file:// URLs or local drive paths). Set it to the root directory of the website being tested (e.g. \"C:/my-site\").",
|
|
135
|
+
"errors.configWebsiteOriginMismatch": "All start URLs must begin with \"websiteOrigin\". Ensure every task \"startUrl\" (and \"qaosConfig.startUrl\") starts with the value of \"websiteOrigin\".",
|
|
133
136
|
|
|
134
137
|
"actions.click": "click element {{index}}",
|
|
135
138
|
"actions.input": "input {{index}}: \"{{text}}\"",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"errors.configMissingProjectId": "Le champ obligatoire \"projectId\" est absent de la configuration (doit être une chaîne non vide).",
|
|
117
117
|
"errors.configEnvFileMustBeString": "Le champ \"envFile\" de la configuration doit être un chemin de type chaîne.",
|
|
118
118
|
"errors.configQaosModeNotBoolean": "Le champ \"qaosMode\" de la configuration doit être un booléen (true ou false).",
|
|
119
|
+
"errors.configMaxBudgetMustBePositiveNumber": "Le champ \"maxBudget\" de la configuration doit être un nombre positif.",
|
|
119
120
|
"errors.configQaosConfigRequired": "La configuration a \"qaosMode: true\" mais l'objet \"qaosConfig\" requis est absent.",
|
|
120
121
|
"errors.configQaosStartUrlRequired": "\"qaosConfig.startUrl\" est obligatoire et doit être une chaîne non vide.",
|
|
121
122
|
"errors.configQaosMaxPagesMustBePositiveInt": "\"qaosConfig.maxPages\" doit être un entier positif.",
|
|
@@ -130,6 +131,8 @@
|
|
|
130
131
|
"errors.aiDecisionParseFailed": "L'agent IA n'a pas réussi à produire une réponse valide après plusieurs tentatives. L'exécution a été arrêtée.",
|
|
131
132
|
"errors.projectNotFound": "Le projet \"{{projectId}}\" n'existe pas ou vous n'y avez pas accès.",
|
|
132
133
|
"errors.projectValidationFailed": "Impossible de vérifier l'accès au projet : {{error}}",
|
|
134
|
+
"errors.configWebsiteOriginRequiredForFilesystem": "\"websiteOrigin\" est requis lorsque des chemins de système de fichiers sont utilisés (URLs file:// ou chemins de lecteur local). Définissez-le sur le répertoire racine du site testé (ex. : \"C:/mon-site\").",
|
|
135
|
+
"errors.configWebsiteOriginMismatch": "Toutes les URLs de départ doivent commencer par \"websiteOrigin\". Assurez-vous que chaque \"startUrl\" (et \"qaosConfig.startUrl\") commence par la valeur de \"websiteOrigin\".",
|
|
133
136
|
|
|
134
137
|
"actions.click": "cliquer sur l'élément {{index}}",
|
|
135
138
|
"actions.input": "saisie {{index}} : \"{{text}}\"",
|
package/dist/prod-config.d.ts
CHANGED
package/dist/prod-config.js
CHANGED
|
@@ -37,9 +37,9 @@ declare class AuthService {
|
|
|
37
37
|
*/
|
|
38
38
|
getAccessToken(): Promise<string | null>;
|
|
39
39
|
/**
|
|
40
|
-
* Get the configured
|
|
40
|
+
* Get the configured website URL
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
getWebsiteUrl(): Promise<string | null>;
|
|
43
43
|
/**
|
|
44
44
|
* Clear authentication (logout)
|
|
45
45
|
*/
|
|
@@ -114,9 +114,9 @@ class AuthService {
|
|
|
114
114
|
return config?.accessToken || null;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
* Get the configured
|
|
117
|
+
* Get the configured website URL
|
|
118
118
|
*/
|
|
119
|
-
async
|
|
119
|
+
async getWebsiteUrl() {
|
|
120
120
|
const config = await this.loadToken();
|
|
121
121
|
return config?.serverUrl || null;
|
|
122
122
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import type { BrowserAction } from '../services/browser-action-service.js';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
id: string;
|
|
5
|
-
description: string;
|
|
6
|
-
status: TaskStatus;
|
|
7
|
-
isSubtask: boolean;
|
|
8
|
-
}
|
|
9
|
-
export interface QaosProgress {
|
|
10
|
-
pagesVisited: number;
|
|
11
|
-
pagesRemaining: number;
|
|
12
|
-
currentPageUrl: string | null;
|
|
13
|
-
tasks: QaosTaskInfo[];
|
|
14
|
-
}
|
|
2
|
+
import type { RunTask, RunTaskStatus, QaosProgress } from './run-ui-state.js';
|
|
3
|
+
export type { RunTask as QaosTaskInfo };
|
|
15
4
|
export interface NextActionsResponse {
|
|
16
5
|
state: {
|
|
17
6
|
thinking: string;
|
|
18
|
-
taskStatus: Record<string,
|
|
7
|
+
taskStatus: Record<string, RunTaskStatus>;
|
|
8
|
+
taskList: RunTask[] | null;
|
|
19
9
|
qaosProgress: QaosProgress | null;
|
|
20
10
|
};
|
|
21
11
|
action: BrowserAction[];
|
|
@@ -5,11 +5,6 @@ export interface RunTask {
|
|
|
5
5
|
id: string;
|
|
6
6
|
description: string;
|
|
7
7
|
status: RunTaskStatus;
|
|
8
|
-
}
|
|
9
|
-
export interface QaosTaskInfo {
|
|
10
|
-
id: string;
|
|
11
|
-
description: string;
|
|
12
|
-
status: RunTaskStatus;
|
|
13
8
|
isSubtask: boolean;
|
|
14
9
|
parentTaskId?: string;
|
|
15
10
|
}
|
|
@@ -17,7 +12,7 @@ export interface QaosProgress {
|
|
|
17
12
|
pagesVisited: number;
|
|
18
13
|
pagesRemaining: number;
|
|
19
14
|
currentPageUrl: string | null;
|
|
20
|
-
tasks:
|
|
15
|
+
tasks: RunTask[];
|
|
21
16
|
}
|
|
22
17
|
export interface RunUiState {
|
|
23
18
|
phase: RunPhase;
|
|
@@ -46,6 +41,7 @@ export type RunUiEvent = {
|
|
|
46
41
|
type: 'server_state';
|
|
47
42
|
thinking: string;
|
|
48
43
|
taskStatus?: Record<string, RunTaskStatus>;
|
|
44
|
+
taskList?: RunTask[];
|
|
49
45
|
actions?: string[];
|
|
50
46
|
qaosProgress?: QaosProgress;
|
|
51
47
|
} | {
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
function isFilesystem(url) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(url).protocol === 'file:' || /^[a-zA-Z]:[\\/]/.test(url);
|
|
4
|
+
}
|
|
5
|
+
catch { /* fall through */ }
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
function normalizePath(url) {
|
|
9
|
+
return url.replace(/\\/g, '/');
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* Validates the parsed config object.
|
|
3
13
|
* Returns an array of translated error messages.
|
|
@@ -18,6 +28,11 @@ export function validateConfig(data, t) {
|
|
|
18
28
|
errors.push(t('errors.configEnvFileMustBeString'));
|
|
19
29
|
if (cfg['qaosMode'] !== undefined && typeof cfg['qaosMode'] !== 'boolean')
|
|
20
30
|
errors.push(t('errors.configQaosModeNotBoolean'));
|
|
31
|
+
if (cfg['maxBudget'] !== undefined &&
|
|
32
|
+
(typeof cfg['maxBudget'] !== 'number' ||
|
|
33
|
+
!Number.isFinite(cfg['maxBudget']) ||
|
|
34
|
+
cfg['maxBudget'] <= 0))
|
|
35
|
+
errors.push(t('errors.configMaxBudgetMustBePositiveNumber'));
|
|
21
36
|
const VALID_AGENTS = ['uiux', 'security'];
|
|
22
37
|
if (cfg['qaosMode'] === true) {
|
|
23
38
|
const qc = cfg['qaosConfig'];
|
|
@@ -51,6 +66,16 @@ export function validateConfig(data, t) {
|
|
|
51
66
|
}));
|
|
52
67
|
}
|
|
53
68
|
}
|
|
69
|
+
if (typeof q['startUrl'] === 'string' &&
|
|
70
|
+
isFilesystem(q['startUrl']) &&
|
|
71
|
+
(typeof cfg['websiteOrigin'] !== 'string' || cfg['websiteOrigin'].trim() === ''))
|
|
72
|
+
errors.push(t('errors.configWebsiteOriginRequiredForFilesystem'));
|
|
73
|
+
if (typeof cfg['websiteOrigin'] === 'string' &&
|
|
74
|
+
cfg['websiteOrigin'].trim() !== '' &&
|
|
75
|
+
typeof q['startUrl'] === 'string' &&
|
|
76
|
+
q['startUrl'].trim() !== '' &&
|
|
77
|
+
!normalizePath(q['startUrl']).startsWith(normalizePath(cfg['websiteOrigin'].trim())))
|
|
78
|
+
errors.push(t('errors.configWebsiteOriginMismatch'));
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
else {
|
|
@@ -61,8 +86,6 @@ export function validateConfig(data, t) {
|
|
|
61
86
|
for (let i = 0; i < cfg['tasks'].length; i++) {
|
|
62
87
|
const task = cfg['tasks'][i];
|
|
63
88
|
const idx = i + 1;
|
|
64
|
-
if (typeof task['id'] !== 'string' || task['id'].trim() === '')
|
|
65
|
-
errors.push(t('errors.configTaskMissingId', { index: idx }));
|
|
66
89
|
if (typeof task['description'] !== 'string' ||
|
|
67
90
|
task['description'].trim() === '')
|
|
68
91
|
errors.push(t('errors.configTaskMissingDescription', { index: idx }));
|
|
@@ -82,6 +105,19 @@ export function validateConfig(data, t) {
|
|
|
82
105
|
}));
|
|
83
106
|
}
|
|
84
107
|
}
|
|
108
|
+
const validStartUrls = cfg['tasks']
|
|
109
|
+
.map((task) => task['startUrl'])
|
|
110
|
+
.filter((u) => typeof u === 'string' && u.trim() !== '');
|
|
111
|
+
if (validStartUrls.some(isFilesystem) &&
|
|
112
|
+
(typeof cfg['websiteOrigin'] !== 'string' || cfg['websiteOrigin'].trim() === ''))
|
|
113
|
+
errors.push(t('errors.configWebsiteOriginRequiredForFilesystem'));
|
|
114
|
+
if (typeof cfg['websiteOrigin'] === 'string' &&
|
|
115
|
+
cfg['websiteOrigin'].trim() !== '' &&
|
|
116
|
+
validStartUrls.length > 0) {
|
|
117
|
+
const wo = normalizePath(cfg['websiteOrigin'].trim());
|
|
118
|
+
if (validStartUrls.some((url) => !normalizePath(url).startsWith(wo)))
|
|
119
|
+
errors.push(t('errors.configWebsiteOriginMismatch'));
|
|
120
|
+
}
|
|
85
121
|
}
|
|
86
122
|
}
|
|
87
123
|
return errors;
|