fraim-hub 2.0.268 → 2.0.269
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/src/ai-hub/catalog.js +14 -0
- package/dist/src/ai-hub/cli.js +70 -8
- package/dist/src/ai-hub/desktop-main.js +21 -9
- package/dist/src/ai-hub/hosts.js +7 -2
- package/dist/src/ai-hub/remote-hub-gateway.js +6 -5
- package/dist/src/ai-hub/server.js +116 -9
- package/dist/src/ai-hub/ui-runtime.js +5 -34
- package/package.json +2 -2
- package/public/ai-hub/script.js +55 -38
- package/public/ai-hub/styles.css +207 -180
|
@@ -7,6 +7,7 @@ exports.summarizeProject = summarizeProject;
|
|
|
7
7
|
exports.discoverEmployeeJobs = discoverEmployeeJobs;
|
|
8
8
|
exports.discoverManagerTemplates = discoverManagerTemplates;
|
|
9
9
|
exports.loadJobPhases = loadJobPhases;
|
|
10
|
+
exports.resolveJobPhaseTransition = resolveJobPhaseTransition;
|
|
10
11
|
exports.loadAllJobPhaseIds = loadAllJobPhaseIds;
|
|
11
12
|
exports.labelForPhaseId = labelForPhaseId;
|
|
12
13
|
exports.getAiHubCategories = getAiHubCategories;
|
|
@@ -470,6 +471,19 @@ function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
|
470
471
|
// this job before appending it to the tracker, so cross-job pollution
|
|
471
472
|
// (agent calling seekMentoring for a different job mid-run) cannot
|
|
472
473
|
// surface stages from elsewhere.
|
|
474
|
+
function resolveJobPhaseTransition(jobId, projectPath, phaseId, outcome, discriminant = 'feature') {
|
|
475
|
+
const stubPath = findJobStubPath(projectPath, jobId);
|
|
476
|
+
if (!stubPath)
|
|
477
|
+
return null;
|
|
478
|
+
const fm = readJobFrontmatter(stubPath);
|
|
479
|
+
if (!fm || !fm.phases)
|
|
480
|
+
return null;
|
|
481
|
+
const phaseDef = fm.phases[phaseId];
|
|
482
|
+
if (!phaseDef)
|
|
483
|
+
return null;
|
|
484
|
+
const edge = outcome === 'complete' ? phaseDef.onSuccess : phaseDef.onFailure;
|
|
485
|
+
return nextPhase(edge, discriminant);
|
|
486
|
+
}
|
|
473
487
|
function loadAllJobPhaseIds(jobId, projectPath) {
|
|
474
488
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
475
489
|
if (!stubPath)
|
package/dist/src/ai-hub/cli.js
CHANGED
|
@@ -36,6 +36,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DESKTOP_HUB_READY_TIMEOUT_ENV = void 0;
|
|
40
|
+
exports.resolveDesktopReadyTimeoutMs = resolveDesktopReadyTimeoutMs;
|
|
39
41
|
exports.waitForDesktopHubReady = waitForDesktopHubReady;
|
|
40
42
|
exports.runHub = runHub;
|
|
41
43
|
// Hub-owned launcher used by the fraim-hub package. Keep this outside
|
|
@@ -207,9 +209,55 @@ function fetchRunningHubVersion(port) {
|
|
|
207
209
|
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
208
210
|
});
|
|
209
211
|
}
|
|
212
|
+
// #1110: the desktop shell's cold-start cost is work this launcher can neither see nor bound.
|
|
213
|
+
// Before the Hub can answer /api/ai-hub/version it pays for Electron process boot, top-level
|
|
214
|
+
// evaluation of the src/ai-hub/server.ts module graph, cert load plus a blocking certutil
|
|
215
|
+
// trust call, and finally the listens. The same path measured 3.3s on an idle machine and
|
|
216
|
+
// 20.5s on a real `npx fraim-hub@latest --restart` immediately after a release, when npm has
|
|
217
|
+
// just written a fresh Electron dist. A fixed budget inside that spread turns a healthy launch
|
|
218
|
+
// into a reported failure, which is what issue #1110 reports.
|
|
219
|
+
//
|
|
220
|
+
// The only positive evidence of a failed launch is the child dying, and that is handled
|
|
221
|
+
// separately below. A live child that has not answered yet is still starting, so the wait is
|
|
222
|
+
// long, says so out loud while it waits, and stays tunable for slower machines.
|
|
223
|
+
exports.DESKTOP_HUB_READY_TIMEOUT_ENV = 'FRAIM_HUB_READY_TIMEOUT_MS';
|
|
224
|
+
const DEFAULT_DESKTOP_HUB_READY_TIMEOUT_MS = 120000;
|
|
225
|
+
const DEFAULT_DESKTOP_HUB_READY_PROGRESS_MS = 15000;
|
|
226
|
+
function resolveDesktopReadyTimeoutMs() {
|
|
227
|
+
const configured = Number(process.env[exports.DESKTOP_HUB_READY_TIMEOUT_ENV]);
|
|
228
|
+
if (Number.isFinite(configured) && configured > 0)
|
|
229
|
+
return configured;
|
|
230
|
+
return DEFAULT_DESKTOP_HUB_READY_TIMEOUT_MS;
|
|
231
|
+
}
|
|
232
|
+
// Whole seconds read best at the real cadence (15s, 30s, 120s), but rounding a sub-second
|
|
233
|
+
// duration to whole seconds prints "after 0s" or repeats "1s elapsed" on consecutive notices.
|
|
234
|
+
// Keep one decimal below this threshold so the number is never wrong at any budget.
|
|
235
|
+
const READY_SECONDS_DECIMAL_BELOW_MS = 10000;
|
|
236
|
+
function formatReadySeconds(durationMs) {
|
|
237
|
+
return durationMs >= READY_SECONDS_DECIMAL_BELOW_MS
|
|
238
|
+
? `${Math.round(durationMs / 1000)}s`
|
|
239
|
+
: `${Math.round(durationMs / 100) / 10}s`;
|
|
240
|
+
}
|
|
241
|
+
function reportDesktopReadyProgress(progress) {
|
|
242
|
+
console.log(`Still waiting for the FRAIM Hub desktop shell on port ${progress.port} `
|
|
243
|
+
+ `(${formatReadySeconds(progress.elapsedMs)} elapsed, giving up at ${formatReadySeconds(progress.timeoutMs)}). `
|
|
244
|
+
+ 'A first launch after an upgrade unpacks Electron, so this can take a while.');
|
|
245
|
+
}
|
|
246
|
+
// Where the Hub could be answering: the shell records its chosen port in the runtime file once
|
|
247
|
+
// it is listening, which is the only way the launcher learns about a port other than the one it
|
|
248
|
+
// asked for. Re-read every poll, because that file is written mid-wait.
|
|
249
|
+
function readinessProbePorts(fraimDir, runtimeId, preferredPort) {
|
|
250
|
+
const runtime = (0, hub_runtime_file_1.readHubRuntimeFile)(fraimDir, runtimeId);
|
|
251
|
+
return runtime?.port && runtime.port !== preferredPort
|
|
252
|
+
? [runtime.port, preferredPort]
|
|
253
|
+
: [preferredPort];
|
|
254
|
+
}
|
|
210
255
|
async function waitForDesktopHubReady(child, preferredPort, options = {}) {
|
|
211
|
-
const timeoutMs = options.timeoutMs ??
|
|
256
|
+
const timeoutMs = options.timeoutMs ?? resolveDesktopReadyTimeoutMs();
|
|
212
257
|
const pollMs = options.pollMs ?? 250;
|
|
258
|
+
const progressAfterMs = options.progressAfterMs ?? DEFAULT_DESKTOP_HUB_READY_PROGRESS_MS;
|
|
259
|
+
const progressIntervalMs = options.progressIntervalMs ?? progressAfterMs;
|
|
260
|
+
const onProgress = options.onProgress ?? reportDesktopReadyProgress;
|
|
213
261
|
const runtimeId = options.runtimeId || 'hub';
|
|
214
262
|
const fraimDir = options.fraimDir || (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
215
263
|
const start = Date.now();
|
|
@@ -220,7 +268,10 @@ async function waitForDesktopHubReady(child, preferredPort, options = {}) {
|
|
|
220
268
|
child.once('error', (error) => {
|
|
221
269
|
childState.error = error;
|
|
222
270
|
});
|
|
223
|
-
|
|
271
|
+
// A dead child is the only positive evidence that the launch failed, so it always wins over
|
|
272
|
+
// the elapsed budget. Checked at the top of every poll and once more after the loop, so a
|
|
273
|
+
// child that dies during the final sleep is still reported as an exit and not as a timeout.
|
|
274
|
+
const failIfChildDied = () => {
|
|
224
275
|
if (childState.error) {
|
|
225
276
|
throw new Error(`FRAIM Hub desktop shell failed to launch: ${childState.error.message}`);
|
|
226
277
|
}
|
|
@@ -228,19 +279,30 @@ async function waitForDesktopHubReady(child, preferredPort, options = {}) {
|
|
|
228
279
|
const detail = childState.exit.signal ? `signal ${childState.exit.signal}` : `exit code ${childState.exit.code ?? 'unknown'}`;
|
|
229
280
|
throw new Error(`FRAIM Hub desktop shell exited before the Hub became ready (${detail}).`);
|
|
230
281
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (const port of
|
|
282
|
+
};
|
|
283
|
+
let nextProgressAtMs = progressAfterMs;
|
|
284
|
+
while (Date.now() - start < timeoutMs) {
|
|
285
|
+
failIfChildDied();
|
|
286
|
+
for (const port of readinessProbePorts(fraimDir, runtimeId, preferredPort)) {
|
|
236
287
|
const version = await fetchRunningHubVersion(port);
|
|
237
288
|
if (version) {
|
|
238
289
|
return { port, version };
|
|
239
290
|
}
|
|
240
291
|
}
|
|
292
|
+
const elapsedMs = Date.now() - start;
|
|
293
|
+
if (elapsedMs >= nextProgressAtMs) {
|
|
294
|
+
onProgress({ elapsedMs, timeoutMs, port: preferredPort });
|
|
295
|
+
nextProgressAtMs = elapsedMs + progressIntervalMs;
|
|
296
|
+
}
|
|
241
297
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
242
298
|
}
|
|
243
|
-
|
|
299
|
+
failIfChildDied();
|
|
300
|
+
const stillRunning = typeof child.pid === 'number'
|
|
301
|
+
? ` The desktop shell (pid ${child.pid}) is still running and may finish starting on its own.`
|
|
302
|
+
: '';
|
|
303
|
+
throw new Error(`Timed out waiting for FRAIM Hub desktop shell to become ready on port ${preferredPort} after ${formatReadySeconds(timeoutMs)}.`
|
|
304
|
+
+ `${stillRunning}`
|
|
305
|
+
+ ` Set ${exports.DESKTOP_HUB_READY_TIMEOUT_ENV} to a larger value if this machine needs longer.`);
|
|
244
306
|
}
|
|
245
307
|
async function reconcileRunningHub(flags, runtimeId = 'hub') {
|
|
246
308
|
const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), runtimeId);
|
|
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.launchDesktopShell = launchDesktopShell;
|
|
7
7
|
const electron_1 = require("electron");
|
|
8
|
-
const electron_updater_1 = require("electron-updater");
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
10
9
|
const fs_1 = __importDefault(require("fs"));
|
|
11
10
|
const server_1 = require("./server");
|
|
@@ -91,8 +90,14 @@ function ensureLoginItem() {
|
|
|
91
90
|
function configureAutoUpdater() {
|
|
92
91
|
if (!electron_1.app.isPackaged)
|
|
93
92
|
return;
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// #1110: electron-updater compiles ~114 files (js-yaml, builder-util-runtime, ...) that a
|
|
94
|
+
// non-packaged `npx fraim-hub` launch never uses, and this whole function returns early
|
|
95
|
+
// there. Requiring it lazily keeps those file reads off the cold-start path, which is what
|
|
96
|
+
// dominates time-to-ready on a freshly unpacked install.
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
98
|
+
const { autoUpdater } = require('electron-updater');
|
|
99
|
+
autoUpdater.autoDownload = true;
|
|
100
|
+
autoUpdater.checkForUpdatesAndNotify().catch((err) => {
|
|
96
101
|
console.warn('[fraim] auto-update check failed:', err);
|
|
97
102
|
});
|
|
98
103
|
}
|
|
@@ -329,13 +334,10 @@ async function launchDesktopShell(options) {
|
|
|
329
334
|
const httpsPort = await (0, server_1.findAvailablePortExcluding)(43092, new Set([httpPort]));
|
|
330
335
|
// Generate (or load cached) self-signed cert for HTTPS.
|
|
331
336
|
// Fast on subsequent launches (file read); ~200ms on first launch (key gen).
|
|
337
|
+
// The cert bundle itself is needed here because server.start() binds the HTTPS listener with
|
|
338
|
+
// it. Trusting it in the OS store is a separate, Word-Online-only concern and happens after
|
|
339
|
+
// the Hub is serving (#1110) - see below.
|
|
332
340
|
const certBundle = await (0, cert_store_1.loadOrCreateCert)();
|
|
333
|
-
try {
|
|
334
|
-
(0, cert_store_1.trustCert)((0, cert_store_1.certPaths)().certPath);
|
|
335
|
-
}
|
|
336
|
-
catch (err) {
|
|
337
|
-
console.warn('[fraim] could not trust localhost certificate:', err);
|
|
338
|
-
}
|
|
339
341
|
server = new server_1.AiHubServer({
|
|
340
342
|
...(options.projectPath ? { projectPath: options.projectPath } : {}),
|
|
341
343
|
// Issue #701: no local DB. Persona/manager-team state resolves through the hosted
|
|
@@ -366,6 +368,16 @@ async function launchDesktopShell(options) {
|
|
|
366
368
|
catch (err) {
|
|
367
369
|
console.warn('[fraim] could not write hub runtime file:', err);
|
|
368
370
|
}
|
|
371
|
+
// #1110: trusting the loopback cert in the OS store is a blocking `certutil` subprocess and
|
|
372
|
+
// is only needed so Word *Online* will render the task pane over HTTPS. The Hub does not need
|
|
373
|
+
// it to serve, so it runs after the server is listening rather than in front of it. Grouped
|
|
374
|
+
// with the Office sideload because both are Word-only first-run housekeeping.
|
|
375
|
+
try {
|
|
376
|
+
(0, cert_store_1.trustCert)((0, cert_store_1.certPaths)().certPath);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
console.warn('[fraim] could not trust localhost certificate:', err);
|
|
380
|
+
}
|
|
369
381
|
ensureWordSideload(resolvedProjectPath, httpsPort);
|
|
370
382
|
const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
|
|
371
383
|
createTray(hubUrl, runtimeId);
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -33,7 +33,6 @@ const os_1 = __importDefault(require("os"));
|
|
|
33
33
|
const path_1 = __importDefault(require("path"));
|
|
34
34
|
const manager_turns_1 = require("./manager-turns");
|
|
35
35
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
36
|
-
const mcp_config_generator_1 = require("../cli/setup/mcp-config-generator");
|
|
37
36
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
38
37
|
const configured_agents_1 = require("./configured-agents");
|
|
39
38
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
@@ -1155,7 +1154,13 @@ function prepareCodexBrowserHome(cdp, env = process.env) {
|
|
|
1155
1154
|
const realConfig = path_1.default.join(real, 'config.toml');
|
|
1156
1155
|
const existing = fs_1.default.existsSync(realConfig) ? fs_1.default.readFileSync(realConfig, 'utf8') : '';
|
|
1157
1156
|
const pwBlock = `[mcp_servers.playwright]\ncommand = "npx"\nargs = ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "${cdp}"]\n`;
|
|
1158
|
-
|
|
1157
|
+
// #1110: this is the only use of mcp-config-generator here, and it runs when a run launches,
|
|
1158
|
+
// never at Hub startup. Importing it at module scope pulled the whole
|
|
1159
|
+
// mcp-server-builder -> mcp-server-registry -> provider-registry -> provider-client -> axios
|
|
1160
|
+
// chain (~30 files) into the graph the Hub must compile before it can listen.
|
|
1161
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1162
|
+
const { mergeTomlMCPServers } = require('../cli/setup/mcp-config-generator');
|
|
1163
|
+
const merged = mergeTomlMCPServers(existing, pwBlock, ['playwright']).content;
|
|
1159
1164
|
fs_1.default.writeFileSync(path_1.default.join(home, 'config.toml'), merged, 'utf8');
|
|
1160
1165
|
// Auth + the session index (so resume can find existing rollouts by thread id).
|
|
1161
1166
|
for (const f of ['auth.json', 'session_index.jsonl', 'history.jsonl']) {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.HttpHubRemoteGateway = void 0;
|
|
7
4
|
exports.resolveFraimRemoteUrl = resolveFraimRemoteUrl;
|
|
8
|
-
const axios_1 = __importDefault(require("axios"));
|
|
9
5
|
function resolveFraimRemoteUrl(explicit) {
|
|
10
6
|
return (explicit || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me').replace(/\/+$/, '');
|
|
11
7
|
}
|
|
@@ -18,7 +14,12 @@ class HttpHubRemoteGateway {
|
|
|
18
14
|
this.baseURL = resolveFraimRemoteUrl(serverUrl);
|
|
19
15
|
}
|
|
20
16
|
client(apiKey) {
|
|
21
|
-
|
|
17
|
+
// #1110: the Hub server module graph is loaded before the local server can listen, and the
|
|
18
|
+
// axios subtree is ~28 files of that graph. Nothing here runs during startup - every caller
|
|
19
|
+
// is a request handler - so axios is required on first use instead of at import time.
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const axios = require('axios');
|
|
22
|
+
return axios.create({
|
|
22
23
|
baseURL: this.baseURL,
|
|
23
24
|
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
|
|
24
25
|
timeout: 10000,
|
|
@@ -68,13 +68,21 @@ const remote_hub_gateway_1 = require("./remote-hub-gateway");
|
|
|
68
68
|
const managed_browser_1 = require("./managed-browser");
|
|
69
69
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
70
70
|
const user_config_1 = require("../cli/utils/user-config");
|
|
71
|
-
const org_publish_1 = require("../cli/utils/org-publish");
|
|
72
71
|
const version_utils_1 = require("../cli/utils/version-utils");
|
|
73
72
|
const hub_latest_version_1 = require("./hub-latest-version");
|
|
74
73
|
const ui_runtime_1 = require("./ui-runtime");
|
|
75
74
|
const ui_cache_1 = require("./ui-cache");
|
|
76
|
-
const semver = __importStar(require("semver"));
|
|
77
75
|
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
76
|
+
// #1110: semver compiles 45 files and this module graph must load before the Hub can listen,
|
|
77
|
+
// so it is required on use rather than at import time. `latest` is null until the best-effort
|
|
78
|
+
// npm registry lookup succeeds, and on that path semver is never needed at all.
|
|
79
|
+
function isNewerPublishedVersion(latest, current) {
|
|
80
|
+
if (!latest)
|
|
81
|
+
return false;
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
83
|
+
const semver = require('semver');
|
|
84
|
+
return !!(semver.valid(current) && semver.valid(latest) && semver.gt(latest, current));
|
|
85
|
+
}
|
|
78
86
|
const BOOTSTRAP_PERSONA_FIRST_PAINT_BUDGET_MS = 250;
|
|
79
87
|
let personaHiringModule;
|
|
80
88
|
let managerHiringModule;
|
|
@@ -956,6 +964,28 @@ function normalizePersistedPhaseHistory(value) {
|
|
|
956
964
|
})
|
|
957
965
|
.filter((entry) => Boolean(entry));
|
|
958
966
|
}
|
|
967
|
+
function normalizePersistedPhaseVisits(value) {
|
|
968
|
+
if (!Array.isArray(value))
|
|
969
|
+
return [];
|
|
970
|
+
return value
|
|
971
|
+
.map((entry) => {
|
|
972
|
+
if (!entry || typeof entry !== 'object')
|
|
973
|
+
return null;
|
|
974
|
+
const raw = entry;
|
|
975
|
+
if (typeof raw.phaseId !== 'string' || !raw.phaseId)
|
|
976
|
+
return null;
|
|
977
|
+
const latestStatus = typeof raw.latestStatus === 'string' && PHASE_STATUSES.has(raw.latestStatus)
|
|
978
|
+
? raw.latestStatus
|
|
979
|
+
: null;
|
|
980
|
+
return {
|
|
981
|
+
phaseId: raw.phaseId,
|
|
982
|
+
enteredAt: typeof raw.enteredAt === 'string' ? raw.enteredAt : new Date().toISOString(),
|
|
983
|
+
latestStatus,
|
|
984
|
+
latestText: typeof raw.latestText === 'string' ? raw.latestText : null,
|
|
985
|
+
};
|
|
986
|
+
})
|
|
987
|
+
.filter((entry) => Boolean(entry));
|
|
988
|
+
}
|
|
959
989
|
function readPersistedRunProjection(conversation) {
|
|
960
990
|
const rawRun = conversation?.run;
|
|
961
991
|
if (!rawRun || typeof rawRun !== 'object')
|
|
@@ -964,6 +994,7 @@ function readPersistedRunProjection(conversation) {
|
|
|
964
994
|
return {
|
|
965
995
|
currentPhase: typeof value.currentPhase === 'string' ? value.currentPhase : null,
|
|
966
996
|
phaseHistory: normalizePersistedPhaseHistory(value.phaseHistory),
|
|
997
|
+
phaseVisits: normalizePersistedPhaseVisits(value.phaseVisits),
|
|
967
998
|
stages: Array.isArray(value.stages) ? value.stages : [],
|
|
968
999
|
totals: value.totals && typeof value.totals === 'object' ? value.totals : null,
|
|
969
1000
|
runDiscriminant: typeof value.runDiscriminant === 'string' ? value.runDiscriminant : null,
|
|
@@ -1140,6 +1171,28 @@ function applyAgentIdentitySignal(run, identity) {
|
|
|
1140
1171
|
run.agentName = identity.agentName;
|
|
1141
1172
|
run.agentModel = identity.agentModel;
|
|
1142
1173
|
}
|
|
1174
|
+
function recordPhaseVisit(run, phaseId, latestStatus, latestText = null) {
|
|
1175
|
+
run.phaseVisits = run.phaseVisits || [];
|
|
1176
|
+
const last = run.phaseVisits[run.phaseVisits.length - 1];
|
|
1177
|
+
if (last && last.phaseId === phaseId) {
|
|
1178
|
+
last.latestStatus = latestStatus;
|
|
1179
|
+
if (latestText)
|
|
1180
|
+
last.latestText = latestText;
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
run.phaseVisits.push({
|
|
1184
|
+
phaseId,
|
|
1185
|
+
enteredAt: new Date().toISOString(),
|
|
1186
|
+
latestStatus,
|
|
1187
|
+
latestText,
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function appendSyntheticPhaseVisit(run, phaseId) {
|
|
1191
|
+
const last = run.phaseVisits?.[run.phaseVisits.length - 1];
|
|
1192
|
+
if (last?.phaseId === phaseId)
|
|
1193
|
+
return;
|
|
1194
|
+
recordPhaseVisit(run, phaseId, 'starting', null);
|
|
1195
|
+
}
|
|
1143
1196
|
function stripStructuredHostPayloads(text) {
|
|
1144
1197
|
return text
|
|
1145
1198
|
.replace(/<delegation_ledger>\s*[\s\S]*?\s*<\/delegation_ledger>/gi, '')
|
|
@@ -1256,7 +1309,25 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
1256
1309
|
};
|
|
1257
1310
|
run.phaseHistory.push(entry);
|
|
1258
1311
|
}
|
|
1312
|
+
recordPhaseVisit(run, signal.phaseId, signal.phaseStatus, signal.findingsText || null);
|
|
1259
1313
|
run.currentPhase = signal.phaseId;
|
|
1314
|
+
if (signal.phaseStatus === 'failure') {
|
|
1315
|
+
const nextPhase = (0, catalog_1.resolveJobPhaseTransition)(run.jobId, run.projectPath, signal.phaseId, 'failure', run.runDiscriminant || 'feature');
|
|
1316
|
+
if (nextPhase) {
|
|
1317
|
+
appendSyntheticPhaseVisit(run, nextPhase);
|
|
1318
|
+
run.currentPhase = nextPhase;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function phaseVisitsForProjection(run) {
|
|
1323
|
+
if (Array.isArray(run.phaseVisits) && run.phaseVisits.length > 0)
|
|
1324
|
+
return run.phaseVisits;
|
|
1325
|
+
return (run.phaseHistory || []).map((entry) => ({
|
|
1326
|
+
phaseId: entry.phaseId,
|
|
1327
|
+
enteredAt: entry.enteredAt,
|
|
1328
|
+
latestStatus: entry.latestStatus,
|
|
1329
|
+
latestText: entry.latestText,
|
|
1330
|
+
}));
|
|
1260
1331
|
}
|
|
1261
1332
|
// Build the stage list for a run. Combines the FSM's reachable path with
|
|
1262
1333
|
// any phases the run has actually visited (in case the run took an
|
|
@@ -1275,7 +1346,8 @@ function deriveStages(run, projectPath) {
|
|
|
1275
1346
|
// phase id NOT in the frontmatter — that last filter prevents
|
|
1276
1347
|
// cross-job pollution from showing up on the tracker.
|
|
1277
1348
|
const allDeclared = (0, catalog_1.loadAllJobPhaseIds)(run.jobId, projectPath);
|
|
1278
|
-
const
|
|
1349
|
+
const visits = phaseVisitsForProjection(run);
|
|
1350
|
+
const visited = visits.map((entry) => entry.phaseId);
|
|
1279
1351
|
const known = new Set(declaredPath.map((p) => p.id));
|
|
1280
1352
|
for (const visitedId of visited) {
|
|
1281
1353
|
if (visitedId === 'starting' || visitedId === '__discriminant__')
|
|
@@ -1291,29 +1363,51 @@ function deriveStages(run, projectPath) {
|
|
|
1291
1363
|
? declaredPath.findIndex((p) => p.id === run.currentPhase)
|
|
1292
1364
|
: -1;
|
|
1293
1365
|
const historyMap = new Map((run.phaseHistory || []).map((e) => [e.phaseId, e]));
|
|
1366
|
+
const visitsByPhase = new Map();
|
|
1367
|
+
for (const visit of visits) {
|
|
1368
|
+
const existing = visitsByPhase.get(visit.phaseId) || [];
|
|
1369
|
+
existing.push(visit);
|
|
1370
|
+
visitsByPhase.set(visit.phaseId, existing);
|
|
1371
|
+
}
|
|
1294
1372
|
const completedWithoutPhaseTelemetry = run.status === 'completed' &&
|
|
1295
1373
|
currentIndex < 0 &&
|
|
1296
|
-
historyMap.size === 0
|
|
1374
|
+
historyMap.size === 0 &&
|
|
1375
|
+
visits.length === 0;
|
|
1297
1376
|
if (completedWithoutPhaseTelemetry) {
|
|
1298
1377
|
return declaredPath.map((phase) => ({ phaseId: phase.id, label: phase.label, state: 'done' }));
|
|
1299
1378
|
}
|
|
1300
1379
|
return declaredPath.map((phase, index) => {
|
|
1301
1380
|
let state;
|
|
1302
1381
|
const entry = historyMap.get(phase.id);
|
|
1382
|
+
const phaseVisits = visitsByPhase.get(phase.id) || [];
|
|
1383
|
+
const latestVisit = phaseVisits[phaseVisits.length - 1] || null;
|
|
1384
|
+
const latestStatus = latestVisit?.latestStatus ?? entry?.latestStatus ?? null;
|
|
1385
|
+
const latestText = latestVisit?.latestText ?? entry?.latestText ?? null;
|
|
1303
1386
|
if (index === currentIndex) {
|
|
1304
1387
|
// If the agent has already reported this phase as 'complete', advance
|
|
1305
1388
|
// its visual state to 'done' so the tracker doesn't look frozen while
|
|
1306
1389
|
// waiting for the agent to start the next phase (e.g. after
|
|
1307
1390
|
// implement-submission completes but before address-feedback starts).
|
|
1308
|
-
state =
|
|
1391
|
+
state = latestStatus === 'complete' ? 'done' : 'current';
|
|
1309
1392
|
}
|
|
1310
|
-
else if (
|
|
1393
|
+
else if (latestStatus === 'complete' ||
|
|
1394
|
+
latestStatus === 'failure' ||
|
|
1395
|
+
latestStatus === 'incomplete' ||
|
|
1396
|
+
(currentIndex >= 0 && index < currentIndex && phaseVisits.length > 0)) {
|
|
1311
1397
|
state = 'done';
|
|
1312
1398
|
}
|
|
1313
1399
|
else {
|
|
1314
1400
|
state = 'upcoming';
|
|
1315
1401
|
}
|
|
1316
|
-
return {
|
|
1402
|
+
return {
|
|
1403
|
+
phaseId: phase.id,
|
|
1404
|
+
label: phase.label,
|
|
1405
|
+
state,
|
|
1406
|
+
visitCount: phaseVisits.length,
|
|
1407
|
+
currentVisit: phase.id === run.currentPhase && phaseVisits.length > 0 ? phaseVisits.length : null,
|
|
1408
|
+
latestStatus,
|
|
1409
|
+
latestText,
|
|
1410
|
+
};
|
|
1317
1411
|
});
|
|
1318
1412
|
}
|
|
1319
1413
|
// ---------------------------------------------------------------------------
|
|
@@ -2522,6 +2616,7 @@ class AiHubServer {
|
|
|
2522
2616
|
stages,
|
|
2523
2617
|
currentPhase: run.currentPhase || null,
|
|
2524
2618
|
phaseHistory: run.phaseHistory || [],
|
|
2619
|
+
phaseVisits: run.phaseVisits || [],
|
|
2525
2620
|
totals: run.totals || null,
|
|
2526
2621
|
runDiscriminant: run.runDiscriminant || null,
|
|
2527
2622
|
},
|
|
@@ -2791,6 +2886,7 @@ class AiHubServer {
|
|
|
2791
2886
|
],
|
|
2792
2887
|
currentPhase: persistedRun?.currentPhase || null,
|
|
2793
2888
|
phaseHistory: persistedRun?.phaseHistory || [],
|
|
2889
|
+
phaseVisits: persistedRun?.phaseVisits || [],
|
|
2794
2890
|
totals: persistedRun?.totals || emptyTotals(),
|
|
2795
2891
|
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
2796
2892
|
artifacts: Array.isArray(conversation.artifacts) ? conversation.artifacts : [],
|
|
@@ -3148,6 +3244,7 @@ class AiHubServer {
|
|
|
3148
3244
|
events: [(0, hosts_1.createHubEvent)('system', `Mandy started delegated workstream ${task.taskId} for ${task.personaKey || task.jobId}.`)],
|
|
3149
3245
|
currentPhase: null,
|
|
3150
3246
|
phaseHistory: [],
|
|
3247
|
+
phaseVisits: [],
|
|
3151
3248
|
totals: emptyTotals(),
|
|
3152
3249
|
lastStatusChangeAt: now,
|
|
3153
3250
|
personaKey: task.personaKey,
|
|
@@ -4151,6 +4248,7 @@ class AiHubServer {
|
|
|
4151
4248
|
],
|
|
4152
4249
|
currentPhase: persistedRun?.currentPhase || null,
|
|
4153
4250
|
phaseHistory: persistedRun?.phaseHistory || [],
|
|
4251
|
+
phaseVisits: persistedRun?.phaseVisits || [],
|
|
4154
4252
|
stages: persistedRun?.stages || [],
|
|
4155
4253
|
totals: persistedRun?.totals || emptyTotals(),
|
|
4156
4254
|
lastStatusChangeAt: now,
|
|
@@ -4423,7 +4521,7 @@ class AiHubServer {
|
|
|
4423
4521
|
this.app.get('/api/ai-hub/version', async (_req, res) => {
|
|
4424
4522
|
const version = (0, version_utils_1.getFraimVersion)();
|
|
4425
4523
|
const latest = await (0, hub_latest_version_1.getLatestPublishedVersion)();
|
|
4426
|
-
const updateAvailable =
|
|
4524
|
+
const updateAvailable = isNewerPublishedVersion(latest, version);
|
|
4427
4525
|
const uiRuntime = await this.resolveHubUiRuntimeResponse();
|
|
4428
4526
|
return res.json({
|
|
4429
4527
|
version,
|
|
@@ -4588,7 +4686,12 @@ class AiHubServer {
|
|
|
4588
4686
|
return res.json({ ok: true, brand: stored, published: false });
|
|
4589
4687
|
}
|
|
4590
4688
|
try {
|
|
4591
|
-
|
|
4689
|
+
// #1110: org-publish drags in pack-git-publish and the whole axios subtree (~28 files)
|
|
4690
|
+
// that the Hub must otherwise compile before it can listen. Only this one brand-publish
|
|
4691
|
+
// handler needs it, so it is required on use rather than at import time.
|
|
4692
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
4693
|
+
const { publishOrgArtifacts } = require('../cli/utils/org-publish');
|
|
4694
|
+
await publishOrgArtifacts([{
|
|
4592
4695
|
relativePath: 'context/org_brand.json',
|
|
4593
4696
|
content: JSON.stringify(stored, null, 2),
|
|
4594
4697
|
}]);
|
|
@@ -4857,6 +4960,7 @@ class AiHubServer {
|
|
|
4857
4960
|
// Issue #347 — seed phase + totals state on creation.
|
|
4858
4961
|
currentPhase: null,
|
|
4859
4962
|
phaseHistory: [],
|
|
4963
|
+
phaseVisits: [],
|
|
4860
4964
|
totals: emptyTotals(),
|
|
4861
4965
|
lastStatusChangeAt: startTimestamp,
|
|
4862
4966
|
personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
|
|
@@ -5288,6 +5392,7 @@ class AiHubServer {
|
|
|
5288
5392
|
// resuming the session starts a fresh job, so the tracker must be blank.
|
|
5289
5393
|
currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
|
|
5290
5394
|
phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
|
|
5395
|
+
phaseVisits: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseVisits || []) : [],
|
|
5291
5396
|
totals: persistedRun?.totals || emptyTotals(),
|
|
5292
5397
|
lastStatusChangeAt: now,
|
|
5293
5398
|
runDiscriminant: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
|
|
@@ -5965,6 +6070,7 @@ class AiHubServer {
|
|
|
5965
6070
|
events: [(0, hosts_1.createHubEvent)('system', `Trigger: ${configuredAgent.label} / ${jobName} from ${context?.sourceApp || 'unknown'} in ${projectPath}`)],
|
|
5966
6071
|
currentPhase: null,
|
|
5967
6072
|
phaseHistory: [],
|
|
6073
|
+
phaseVisits: [],
|
|
5968
6074
|
totals: emptyTotals(),
|
|
5969
6075
|
lastStatusChangeAt: startTimestamp,
|
|
5970
6076
|
personaKey: getHubPersonaForJob(jobName),
|
|
@@ -6116,6 +6222,7 @@ class AiHubServer {
|
|
|
6116
6222
|
events: [(0, hosts_1.createHubEvent)('system', `Triggered by deployment: ${deployment.label} (${deployment.type}) using ${configuredAgent.label}`)],
|
|
6117
6223
|
currentPhase: null,
|
|
6118
6224
|
phaseHistory: [],
|
|
6225
|
+
phaseVisits: [],
|
|
6119
6226
|
totals: emptyTotals(),
|
|
6120
6227
|
lastStatusChangeAt: startTimestamp,
|
|
6121
6228
|
personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
|
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
@@ -41,7 +8,9 @@ exports.evaluateHubUiManifestCompatibility = evaluateHubUiManifestCompatibility;
|
|
|
41
8
|
exports.resolveHubUiRuntime = resolveHubUiRuntime;
|
|
42
9
|
const fs_1 = __importDefault(require("fs"));
|
|
43
10
|
const path_1 = __importDefault(require("path"));
|
|
44
|
-
|
|
11
|
+
// #1110: semver compiles 45 files. This module is on the AI Hub server's import graph, which
|
|
12
|
+
// must load before the Hub can listen, and the only use below runs when a UI release manifest
|
|
13
|
+
// is evaluated - never during startup. Required on use to keep it off the cold-start path.
|
|
45
14
|
const version_utils_1 = require("../cli/utils/version-utils");
|
|
46
15
|
const ui_cache_1 = require("./ui-cache");
|
|
47
16
|
const ui_manifest_client_1 = require("./ui-manifest-client");
|
|
@@ -72,6 +41,8 @@ function evaluateHubUiManifestCompatibility(manifest, bridge) {
|
|
|
72
41
|
if (manifest.schemaVersion !== 1) {
|
|
73
42
|
return { compatible: false, updateRequired: true, reason: `schema-version:${manifest.schemaVersion}` };
|
|
74
43
|
}
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
+
const semver = require('semver');
|
|
75
46
|
if (semver.valid(manifest.minBridgeVersion) && semver.valid(bridge.bridgeVersion) && semver.lt(bridge.bridgeVersion, manifest.minBridgeVersion)) {
|
|
76
47
|
return { compatible: false, updateRequired: true, reason: `min-bridge-version:${manifest.minBridgeVersion}` };
|
|
77
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.269",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"electron": "^41.2.2",
|
|
164
164
|
"electron-updater": "^6.8.9",
|
|
165
165
|
"express": "^5.2.1",
|
|
166
|
-
"fraim": "2.0.
|
|
166
|
+
"fraim": "2.0.269",
|
|
167
167
|
"mongodb": "^7.0.0",
|
|
168
168
|
"node-cron": "4.2.1",
|
|
169
169
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -760,19 +760,19 @@ function ensureTaskPaneLauncher() {
|
|
|
760
760
|
root.appendChild(actions);
|
|
761
761
|
|
|
762
762
|
document.body.insertBefore(root, document.body.firstChild);
|
|
763
|
-
document.getElementById('task-pane-project-select')?.addEventListener('change', (event) => {
|
|
764
|
-
const statusEl = document.getElementById('task-pane-start-status');
|
|
765
|
-
const project = taskPaneProjectEntries().find((entry) => entry.folderPath === event.target.value);
|
|
766
|
-
if (statusEl && project) statusEl.textContent = `Project: ${project.name || friendlyProjectShortName(project.folderPath)}`;
|
|
767
|
-
});
|
|
768
|
-
document.getElementById('task-pane-employee-select')?.addEventListener('change', (event) => {
|
|
769
|
-
const value = event.target && event.target.value;
|
|
770
|
-
if (value && hubConfiguredAgents().some((agent) => agent.id === value)) {
|
|
771
|
-
state.selectedEmployeeId = value;
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
return root;
|
|
775
|
-
}
|
|
763
|
+
document.getElementById('task-pane-project-select')?.addEventListener('change', (event) => {
|
|
764
|
+
const statusEl = document.getElementById('task-pane-start-status');
|
|
765
|
+
const project = taskPaneProjectEntries().find((entry) => entry.folderPath === event.target.value);
|
|
766
|
+
if (statusEl && project) statusEl.textContent = `Project: ${project.name || friendlyProjectShortName(project.folderPath)}`;
|
|
767
|
+
});
|
|
768
|
+
document.getElementById('task-pane-employee-select')?.addEventListener('change', (event) => {
|
|
769
|
+
const value = event.target && event.target.value;
|
|
770
|
+
if (value && hubConfiguredAgents().some((agent) => agent.id === value)) {
|
|
771
|
+
state.selectedEmployeeId = value;
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
return root;
|
|
775
|
+
}
|
|
776
776
|
|
|
777
777
|
function renderTaskPaneLauncher() {
|
|
778
778
|
if (!isTaskPaneSurface() || !state.bootstrap) return;
|
|
@@ -857,23 +857,23 @@ async function taskPaneStartSelectedJob() {
|
|
|
857
857
|
if (start) start.disabled = true;
|
|
858
858
|
if (statusEl) statusEl.textContent = 'Starting...';
|
|
859
859
|
try {
|
|
860
|
-
const result = await startRun(job, instructions ? instructions.value : '', employeeId, undefined, 'projects');
|
|
861
|
-
if (result && result.ok) {
|
|
862
|
-
taskPaneLauncherForcedOpen = false;
|
|
863
|
-
if (statusEl) statusEl.textContent = 'Started.';
|
|
864
|
-
renderActive();
|
|
865
|
-
syncTaskPaneSurfaceMode();
|
|
866
|
-
} else if (statusEl) {
|
|
867
|
-
statusEl.textContent = (result && result.error) || 'Could not start.';
|
|
868
|
-
}
|
|
860
|
+
const result = await startRun(job, instructions ? instructions.value : '', employeeId, undefined, 'projects');
|
|
861
|
+
if (result && result.ok) {
|
|
862
|
+
taskPaneLauncherForcedOpen = false;
|
|
863
|
+
if (statusEl) statusEl.textContent = 'Started.';
|
|
864
|
+
renderActive();
|
|
865
|
+
syncTaskPaneSurfaceMode();
|
|
866
|
+
} else if (statusEl) {
|
|
867
|
+
statusEl.textContent = (result && result.error) || 'Could not start.';
|
|
868
|
+
}
|
|
869
869
|
} catch (error) {
|
|
870
870
|
if (statusEl) statusEl.textContent = 'Could not start.';
|
|
871
871
|
console.warn('[ai-hub] task-pane start failed:', error);
|
|
872
|
-
} finally {
|
|
873
|
-
renderTaskPaneLauncher();
|
|
874
|
-
syncTaskPaneSurfaceMode();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
872
|
+
} finally {
|
|
873
|
+
renderTaskPaneLauncher();
|
|
874
|
+
syncTaskPaneSurfaceMode();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
877
|
|
|
878
878
|
// ---------------------------------------------------------------------------
|
|
879
879
|
// Concept popover population (drives the welcome-line "see ... jobs" lists)
|
|
@@ -1037,18 +1037,18 @@ function normalizeGeminiConversationMessages(conv) {
|
|
|
1037
1037
|
// request 413s, the error is swallowed, and the client-owned fields in the SAME
|
|
1038
1038
|
// payload (e.g. pauseReason='done' from "Mark complete") never persist. Strip them
|
|
1039
1039
|
// here; the server's PUT handler merges them back from the stored record.
|
|
1040
|
-
const SERVER_OWNED_CONV_FIELDS = ['messages', 'events', 'artifacts', 'run', 'delegation', 'handoffSummary'];
|
|
1041
|
-
// Issue #1090: `_bodyFetched` records that THIS CLIENT successfully fetched the
|
|
1042
|
-
// body, which is a different claim from "the record has body fields". A run can
|
|
1043
|
-
// legitimately finish with no messages, events, artifacts, run, or delegation;
|
|
1040
|
+
const SERVER_OWNED_CONV_FIELDS = ['messages', 'events', 'artifacts', 'run', 'delegation', 'handoffSummary'];
|
|
1041
|
+
// Issue #1090: `_bodyFetched` records that THIS CLIENT successfully fetched the
|
|
1042
|
+
// body, which is a different claim from "the record has body fields". A run can
|
|
1043
|
+
// legitimately finish with no messages, events, artifacts, run, or delegation;
|
|
1044
1044
|
// before this marker existed such a body was indistinguishable from one that had
|
|
1045
1045
|
// never been hydrated, so the transcript hung on "Loading conversation…" and
|
|
1046
1046
|
// re-fetched on every poll tick forever.
|
|
1047
1047
|
//
|
|
1048
|
-
// It is listed in CLIENT_ONLY_CONV_FIELDS so `cleanConversationHeader()` strips
|
|
1049
|
-
// any inbound value. That is what preserves issue #913: a marker arriving over
|
|
1050
|
-
// the wire is never trusted, only one this client set after a real fetch.
|
|
1051
|
-
const CLIENT_ONLY_CONV_FIELDS = ['_bodyLoaded', '_bodyFetched', '_stopping'];
|
|
1048
|
+
// It is listed in CLIENT_ONLY_CONV_FIELDS so `cleanConversationHeader()` strips
|
|
1049
|
+
// any inbound value. That is what preserves issue #913: a marker arriving over
|
|
1050
|
+
// the wire is never trusted, only one this client set after a real fetch.
|
|
1051
|
+
const CLIENT_ONLY_CONV_FIELDS = ['_bodyLoaded', '_bodyFetched', '_stopping'];
|
|
1052
1052
|
function slimConversationForPersist(conv) {
|
|
1053
1053
|
if (!conv || typeof conv !== 'object') return conv;
|
|
1054
1054
|
const slim = { ...conv };
|
|
@@ -3914,7 +3914,7 @@ function renderTracker(conv) {
|
|
|
3914
3914
|
const rowCapacity = trackerRowCapacity(tracker, stages);
|
|
3915
3915
|
|
|
3916
3916
|
// Cache key so we only rebuild the DOM when the stage data changes.
|
|
3917
|
-
const key = stages.map((s) => `${s.phaseId}:${s.state}`).join('|') +
|
|
3917
|
+
const key = stages.map((s) => `${s.phaseId}:${s.state}:${s.visitCount || 0}:${s.currentVisit || 0}:${s.latestStatus || ''}`).join('|') +
|
|
3918
3918
|
'#' + (conv.run.currentPhase || '') +
|
|
3919
3919
|
'#' + rowCapacity;
|
|
3920
3920
|
if (key === renderedTrackerKey) {
|
|
@@ -3936,7 +3936,9 @@ function renderTracker(conv) {
|
|
|
3936
3936
|
rowsHost.parentNode.insertBefore(activeLabelHost, els['tracker-note']);
|
|
3937
3937
|
}
|
|
3938
3938
|
const currentStage = stages.find((s) => s.state === 'current');
|
|
3939
|
-
activeLabelHost.textContent = currentStage
|
|
3939
|
+
activeLabelHost.textContent = currentStage
|
|
3940
|
+
? `${currentStage.label}${currentStage.currentVisit && currentStage.currentVisit > 1 ? ` - visit ${currentStage.currentVisit}` : ''}`
|
|
3941
|
+
: '';
|
|
3940
3942
|
// The pizza tracker is always a single horizontal row. When the pane is too
|
|
3941
3943
|
// narrow to fit every stage label, collapse to circles + the active-stage
|
|
3942
3944
|
// label (CSS .tracker-compact) instead of wrapping onto stacked rows.
|
|
@@ -3948,12 +3950,21 @@ function renderTracker(conv) {
|
|
|
3948
3950
|
const stageEl = document.createElement('div');
|
|
3949
3951
|
stageEl.className = 'stage ' + stage.state;
|
|
3950
3952
|
const tooltipText = buildStageTooltip(conv, stage);
|
|
3951
|
-
if (tooltipText)
|
|
3953
|
+
if (tooltipText) {
|
|
3954
|
+
stageEl.setAttribute('title', tooltipText);
|
|
3955
|
+
stageEl.setAttribute('aria-label', tooltipText);
|
|
3956
|
+
}
|
|
3952
3957
|
const circle = document.createElement('span');
|
|
3953
3958
|
circle.className = 'stage-circle';
|
|
3954
3959
|
if (stage.state === 'done') circle.textContent = '✓';
|
|
3955
3960
|
else if (stage.state === 'current') circle.textContent = '●';
|
|
3956
3961
|
else circle.textContent = String(index + 1);
|
|
3962
|
+
if (stage.visitCount && stage.visitCount > 1) {
|
|
3963
|
+
const badge = document.createElement('span');
|
|
3964
|
+
badge.className = 'stage-visit-badge';
|
|
3965
|
+
badge.textContent = `x${stage.visitCount}`;
|
|
3966
|
+
circle.appendChild(badge);
|
|
3967
|
+
}
|
|
3957
3968
|
const label = document.createElement('span');
|
|
3958
3969
|
label.className = 'stage-label';
|
|
3959
3970
|
label.textContent = stage.label;
|
|
@@ -3967,6 +3978,11 @@ function renderTracker(conv) {
|
|
|
3967
3978
|
function buildStageTooltip(conv, stage) {
|
|
3968
3979
|
const history = (conv.run && conv.run.phaseHistory) || [];
|
|
3969
3980
|
const entry = history.find((h) => h.phaseId === stage.phaseId);
|
|
3981
|
+
const details = [stage.phaseId];
|
|
3982
|
+
if (stage.visitCount && stage.visitCount > 1) details.push(`visit ${stage.currentVisit || stage.visitCount} of ${stage.visitCount}`);
|
|
3983
|
+
if (stage.latestStatus) details.push(stage.latestStatus);
|
|
3984
|
+
if (stage.latestText) details.push(stage.latestText);
|
|
3985
|
+
if (details.length > 1) return details.join(' - ');
|
|
3970
3986
|
const findings = entry && entry.latestText;
|
|
3971
3987
|
if (findings) return `${stage.phaseId} · ${findings}`;
|
|
3972
3988
|
return stage.phaseId;
|
|
@@ -7696,6 +7712,7 @@ function foldRunIntoConversation(conv, run) {
|
|
|
7696
7712
|
stages: run.stages || [],
|
|
7697
7713
|
currentPhase: run.currentPhase || null,
|
|
7698
7714
|
phaseHistory: run.phaseHistory || [],
|
|
7715
|
+
phaseVisits: run.phaseVisits || [],
|
|
7699
7716
|
totals: run.totals || null,
|
|
7700
7717
|
runDiscriminant: run.runDiscriminant || null,
|
|
7701
7718
|
};
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -2380,6 +2380,26 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
2380
2380
|
color: #fff;
|
|
2381
2381
|
box-shadow: 0 0 0 4px rgba(176, 132, 66, 0.15);
|
|
2382
2382
|
}
|
|
2383
|
+
.tracker .stage-visit-badge {
|
|
2384
|
+
position: absolute;
|
|
2385
|
+
right: -13px;
|
|
2386
|
+
top: -10px;
|
|
2387
|
+
min-width: 18px;
|
|
2388
|
+
height: 16px;
|
|
2389
|
+
padding: 0 4px;
|
|
2390
|
+
border-radius: 999px;
|
|
2391
|
+
display: inline-flex;
|
|
2392
|
+
align-items: center;
|
|
2393
|
+
justify-content: center;
|
|
2394
|
+
border: 1px solid var(--surface);
|
|
2395
|
+
background: var(--text);
|
|
2396
|
+
color: var(--surface);
|
|
2397
|
+
font-size: 9px;
|
|
2398
|
+
font-weight: 800;
|
|
2399
|
+
line-height: 1;
|
|
2400
|
+
z-index: 3;
|
|
2401
|
+
box-sizing: border-box;
|
|
2402
|
+
}
|
|
2383
2403
|
.tracker .stage-label {
|
|
2384
2404
|
font-size: 10px;
|
|
2385
2405
|
color: var(--muted);
|
|
@@ -2426,6 +2446,13 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
2426
2446
|
border-width: 1px;
|
|
2427
2447
|
font-size: 10px;
|
|
2428
2448
|
}
|
|
2449
|
+
.tracker .stage-visit-badge {
|
|
2450
|
+
right: -12px;
|
|
2451
|
+
top: -9px;
|
|
2452
|
+
min-width: 17px;
|
|
2453
|
+
height: 15px;
|
|
2454
|
+
font-size: 8px;
|
|
2455
|
+
}
|
|
2429
2456
|
.tracker .stage::before { top: 10px; }
|
|
2430
2457
|
.tracker-active-label { display: block; }
|
|
2431
2458
|
}
|
|
@@ -2866,27 +2893,27 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
2866
2893
|
body:is([data-surface="task-pane"],[data-surface="extension"]). No media queries — this is a surface
|
|
2867
2894
|
flag, not a viewport breakpoint. */
|
|
2868
2895
|
|
|
2869
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) {
|
|
2870
|
-
font-size: 13px;
|
|
2871
|
-
overflow: auto;
|
|
2872
|
-
background: var(--bg);
|
|
2873
|
-
color: var(--text);
|
|
2874
|
-
}
|
|
2896
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) {
|
|
2897
|
+
font-size: 13px;
|
|
2898
|
+
overflow: auto;
|
|
2899
|
+
background: var(--bg);
|
|
2900
|
+
color: var(--text);
|
|
2901
|
+
}
|
|
2875
2902
|
body:is([data-surface="task-pane"],[data-surface="extension"]) .hub-tabs,
|
|
2876
2903
|
body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .hub-area,
|
|
2877
2904
|
body:is([data-surface="task-pane"],[data-surface="extension"]) .proj-tabs {
|
|
2878
2905
|
display: none !important;
|
|
2879
2906
|
}
|
|
2880
2907
|
.task-pane-launcher { display: none; }
|
|
2881
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher {
|
|
2882
|
-
display: flex;
|
|
2883
|
-
flex-direction: column;
|
|
2884
|
-
gap: 10px;
|
|
2885
|
-
padding: 12px;
|
|
2886
|
-
border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
|
|
2887
|
-
background: var(--surface, #fff);
|
|
2888
|
-
color: var(--text, #171717);
|
|
2889
|
-
}
|
|
2908
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher {
|
|
2909
|
+
display: flex;
|
|
2910
|
+
flex-direction: column;
|
|
2911
|
+
gap: 10px;
|
|
2912
|
+
padding: 12px;
|
|
2913
|
+
border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
|
|
2914
|
+
background: var(--surface, #fff);
|
|
2915
|
+
color: var(--text, #171717);
|
|
2916
|
+
}
|
|
2890
2917
|
body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active #area-projects {
|
|
2891
2918
|
display: flex !important;
|
|
2892
2919
|
min-height: 100vh;
|
|
@@ -2925,11 +2952,11 @@ body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-act
|
|
|
2925
2952
|
font-weight: 650;
|
|
2926
2953
|
color: var(--text, #171717);
|
|
2927
2954
|
}
|
|
2928
|
-
.tp-launcher-context,
|
|
2929
|
-
.tp-launcher-status {
|
|
2930
|
-
min-height: 16px;
|
|
2931
|
-
font-size: 11px;
|
|
2932
|
-
color: var(--muted);
|
|
2955
|
+
.tp-launcher-context,
|
|
2956
|
+
.tp-launcher-status {
|
|
2957
|
+
min-height: 16px;
|
|
2958
|
+
font-size: 11px;
|
|
2959
|
+
color: var(--muted);
|
|
2933
2960
|
overflow: hidden;
|
|
2934
2961
|
text-overflow: ellipsis;
|
|
2935
2962
|
white-space: nowrap;
|
|
@@ -2943,27 +2970,27 @@ body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-act
|
|
|
2943
2970
|
font-weight: 600;
|
|
2944
2971
|
color: var(--muted);
|
|
2945
2972
|
}
|
|
2946
|
-
.tp-launcher-field select,
|
|
2947
|
-
.tp-launcher-field textarea {
|
|
2948
|
-
width: 100%;
|
|
2949
|
-
min-width: 0;
|
|
2950
|
-
border: 1px solid var(--line, rgba(0,0,0,0.12));
|
|
2951
|
-
border-radius: 8px;
|
|
2952
|
-
background: var(--surface, #fff);
|
|
2953
|
-
color: var(--text, #171717);
|
|
2954
|
-
font: inherit;
|
|
2955
|
-
font-size: 12px;
|
|
2956
|
-
}
|
|
2957
|
-
.tp-launcher-field select:focus,
|
|
2958
|
-
.tp-launcher-field textarea:focus {
|
|
2959
|
-
outline: 2px solid var(--accent, #0071e3);
|
|
2960
|
-
outline-offset: 1px;
|
|
2961
|
-
border-color: var(--accent, #0071e3);
|
|
2962
|
-
}
|
|
2963
|
-
.tp-launcher-field textarea::placeholder {
|
|
2964
|
-
color: var(--muted);
|
|
2965
|
-
opacity: 1;
|
|
2966
|
-
}
|
|
2973
|
+
.tp-launcher-field select,
|
|
2974
|
+
.tp-launcher-field textarea {
|
|
2975
|
+
width: 100%;
|
|
2976
|
+
min-width: 0;
|
|
2977
|
+
border: 1px solid var(--line, rgba(0,0,0,0.12));
|
|
2978
|
+
border-radius: 8px;
|
|
2979
|
+
background: var(--surface, #fff);
|
|
2980
|
+
color: var(--text, #171717);
|
|
2981
|
+
font: inherit;
|
|
2982
|
+
font-size: 12px;
|
|
2983
|
+
}
|
|
2984
|
+
.tp-launcher-field select:focus,
|
|
2985
|
+
.tp-launcher-field textarea:focus {
|
|
2986
|
+
outline: 2px solid var(--accent, #0071e3);
|
|
2987
|
+
outline-offset: 1px;
|
|
2988
|
+
border-color: var(--accent, #0071e3);
|
|
2989
|
+
}
|
|
2990
|
+
.tp-launcher-field textarea::placeholder {
|
|
2991
|
+
color: var(--muted);
|
|
2992
|
+
opacity: 1;
|
|
2993
|
+
}
|
|
2967
2994
|
.tp-launcher-field select {
|
|
2968
2995
|
height: 34px;
|
|
2969
2996
|
padding: 0 8px;
|
|
@@ -2974,98 +3001,98 @@ body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-act
|
|
|
2974
3001
|
padding: 8px;
|
|
2975
3002
|
line-height: 1.35;
|
|
2976
3003
|
}
|
|
2977
|
-
.tp-launcher-actions {
|
|
2978
|
-
display: grid;
|
|
2979
|
-
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
2980
|
-
align-items: center;
|
|
2981
|
-
gap: 8px;
|
|
2982
|
-
}
|
|
2983
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-start-job,
|
|
2984
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-current-job {
|
|
2985
|
-
min-height: 34px;
|
|
2986
|
-
padding: 7px 12px;
|
|
2987
|
-
border-radius: 8px;
|
|
2988
|
-
font-size: 12px;
|
|
2989
|
-
}
|
|
2990
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-current-job[hidden] {
|
|
2991
|
-
display: none;
|
|
2992
|
-
}
|
|
2993
|
-
.task-pane-run-nav {
|
|
2994
|
-
display: none;
|
|
2995
|
-
}
|
|
2996
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active .task-pane-run-nav {
|
|
2997
|
-
display: flex;
|
|
2998
|
-
align-items: center;
|
|
2999
|
-
justify-content: space-between;
|
|
3000
|
-
gap: 10px;
|
|
3001
|
-
min-width: 0;
|
|
3002
|
-
padding: 10px 12px;
|
|
3003
|
-
border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
|
|
3004
|
-
background: var(--surface, #fff);
|
|
3005
|
-
color: var(--text, #171717);
|
|
3006
|
-
}
|
|
3007
|
-
.tp-run-copy {
|
|
3008
|
-
min-width: 0;
|
|
3009
|
-
display: flex;
|
|
3010
|
-
flex-direction: column;
|
|
3011
|
-
gap: 1px;
|
|
3012
|
-
}
|
|
3013
|
-
.tp-run-eyebrow,
|
|
3014
|
-
.tp-run-meta {
|
|
3015
|
-
font-size: 11px;
|
|
3016
|
-
line-height: 1.25;
|
|
3017
|
-
color: var(--muted);
|
|
3018
|
-
}
|
|
3019
|
-
.tp-run-title {
|
|
3020
|
-
min-width: 0;
|
|
3021
|
-
font-size: 13px;
|
|
3022
|
-
line-height: 1.3;
|
|
3023
|
-
font-weight: 650;
|
|
3024
|
-
color: var(--text, #171717);
|
|
3025
|
-
overflow: hidden;
|
|
3026
|
-
text-overflow: ellipsis;
|
|
3027
|
-
white-space: nowrap;
|
|
3028
|
-
}
|
|
3029
|
-
.tp-run-actions {
|
|
3030
|
-
flex: 0 0 auto;
|
|
3031
|
-
}
|
|
3032
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-new-job {
|
|
3033
|
-
min-height: 34px;
|
|
3034
|
-
padding: 7px 12px;
|
|
3035
|
-
border-radius: 8px;
|
|
3036
|
-
font-size: 12px;
|
|
3037
|
-
white-space: nowrap;
|
|
3038
|
-
}
|
|
3039
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) {
|
|
3040
|
-
background: var(--bg);
|
|
3041
|
-
}
|
|
3042
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher,
|
|
3043
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active .task-pane-run-nav {
|
|
3044
|
-
background: #242424;
|
|
3045
|
-
border-color: rgba(255, 255, 255, 0.16);
|
|
3046
|
-
}
|
|
3047
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select,
|
|
3048
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field textarea {
|
|
3049
|
-
background: #171717;
|
|
3050
|
-
color: #f2f2f2;
|
|
3051
|
-
border-color: rgba(255, 255, 255, 0.22);
|
|
3052
|
-
}
|
|
3053
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select option {
|
|
3054
|
-
background: #171717;
|
|
3055
|
-
color: #f2f2f2;
|
|
3056
|
-
}
|
|
3057
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-context,
|
|
3058
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-status,
|
|
3059
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field,
|
|
3060
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-eyebrow,
|
|
3061
|
-
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-meta {
|
|
3062
|
-
color: #c7c7c7;
|
|
3063
|
-
}
|
|
3064
|
-
body:is([data-surface="task-pane"],[data-surface="extension"]) .page {
|
|
3065
|
-
padding: 10px 12px 8px;
|
|
3066
|
-
gap: 10px;
|
|
3067
|
-
height: auto;
|
|
3068
|
-
min-height: 100vh;
|
|
3004
|
+
.tp-launcher-actions {
|
|
3005
|
+
display: grid;
|
|
3006
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
3007
|
+
align-items: center;
|
|
3008
|
+
gap: 8px;
|
|
3009
|
+
}
|
|
3010
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-start-job,
|
|
3011
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-current-job {
|
|
3012
|
+
min-height: 34px;
|
|
3013
|
+
padding: 7px 12px;
|
|
3014
|
+
border-radius: 8px;
|
|
3015
|
+
font-size: 12px;
|
|
3016
|
+
}
|
|
3017
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-current-job[hidden] {
|
|
3018
|
+
display: none;
|
|
3019
|
+
}
|
|
3020
|
+
.task-pane-run-nav {
|
|
3021
|
+
display: none;
|
|
3022
|
+
}
|
|
3023
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active .task-pane-run-nav {
|
|
3024
|
+
display: flex;
|
|
3025
|
+
align-items: center;
|
|
3026
|
+
justify-content: space-between;
|
|
3027
|
+
gap: 10px;
|
|
3028
|
+
min-width: 0;
|
|
3029
|
+
padding: 10px 12px;
|
|
3030
|
+
border-bottom: 1px solid var(--line, rgba(0,0,0,0.07));
|
|
3031
|
+
background: var(--surface, #fff);
|
|
3032
|
+
color: var(--text, #171717);
|
|
3033
|
+
}
|
|
3034
|
+
.tp-run-copy {
|
|
3035
|
+
min-width: 0;
|
|
3036
|
+
display: flex;
|
|
3037
|
+
flex-direction: column;
|
|
3038
|
+
gap: 1px;
|
|
3039
|
+
}
|
|
3040
|
+
.tp-run-eyebrow,
|
|
3041
|
+
.tp-run-meta {
|
|
3042
|
+
font-size: 11px;
|
|
3043
|
+
line-height: 1.25;
|
|
3044
|
+
color: var(--muted);
|
|
3045
|
+
}
|
|
3046
|
+
.tp-run-title {
|
|
3047
|
+
min-width: 0;
|
|
3048
|
+
font-size: 13px;
|
|
3049
|
+
line-height: 1.3;
|
|
3050
|
+
font-weight: 650;
|
|
3051
|
+
color: var(--text, #171717);
|
|
3052
|
+
overflow: hidden;
|
|
3053
|
+
text-overflow: ellipsis;
|
|
3054
|
+
white-space: nowrap;
|
|
3055
|
+
}
|
|
3056
|
+
.tp-run-actions {
|
|
3057
|
+
flex: 0 0 auto;
|
|
3058
|
+
}
|
|
3059
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-new-job {
|
|
3060
|
+
min-height: 34px;
|
|
3061
|
+
padding: 7px 12px;
|
|
3062
|
+
border-radius: 8px;
|
|
3063
|
+
font-size: 12px;
|
|
3064
|
+
white-space: nowrap;
|
|
3065
|
+
}
|
|
3066
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) {
|
|
3067
|
+
background: var(--bg);
|
|
3068
|
+
}
|
|
3069
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]):not(.task-pane-run-active) .task-pane-launcher,
|
|
3070
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]).task-pane-run-active .task-pane-run-nav {
|
|
3071
|
+
background: #242424;
|
|
3072
|
+
border-color: rgba(255, 255, 255, 0.16);
|
|
3073
|
+
}
|
|
3074
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select,
|
|
3075
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field textarea {
|
|
3076
|
+
background: #171717;
|
|
3077
|
+
color: #f2f2f2;
|
|
3078
|
+
border-color: rgba(255, 255, 255, 0.22);
|
|
3079
|
+
}
|
|
3080
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field select option {
|
|
3081
|
+
background: #171717;
|
|
3082
|
+
color: #f2f2f2;
|
|
3083
|
+
}
|
|
3084
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-context,
|
|
3085
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-status,
|
|
3086
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-launcher-field,
|
|
3087
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-eyebrow,
|
|
3088
|
+
:root[data-theme="dark"] body:is([data-surface="task-pane"],[data-surface="extension"]) .tp-run-meta {
|
|
3089
|
+
color: #c7c7c7;
|
|
3090
|
+
}
|
|
3091
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) .page {
|
|
3092
|
+
padding: 10px 12px 8px;
|
|
3093
|
+
gap: 10px;
|
|
3094
|
+
height: auto;
|
|
3095
|
+
min-height: 100vh;
|
|
3069
3096
|
}
|
|
3070
3097
|
/* Hide the full-width welcome header — task pane has no room for it */
|
|
3071
3098
|
body:is([data-surface="task-pane"],[data-surface="extension"]) .header { display: none; }
|
|
@@ -3451,8 +3478,8 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
3451
3478
|
.pending-banner { background: var(--warn-soft); border: 1px solid rgba(176,132,66,.25); border-radius: 12px; padding: 12px 16px; display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
|
|
3452
3479
|
|
|
3453
3480
|
/* Projects sub-tabs — HTML uses .proj-tabs (not .proj-tabs-bar) and .ptab-add (not .ptab-add-btn) */
|
|
3454
|
-
.proj-tabs, .proj-tabs-bar { display: flex; align-items: center; background: var(--surface); border-bottom: 1px solid var(--line); padding: 0 8px 0 0; flex-shrink: 0; overflow: hidden; }
|
|
3455
|
-
#proj-tab-list { display: flex; align-items: center; min-width: 0; flex: 1 1 auto; overflow-x: auto; overflow-y: hidden; white-space: nowrap; }
|
|
3481
|
+
.proj-tabs, .proj-tabs-bar { display: flex; align-items: center; background: var(--surface); border-bottom: 1px solid var(--line); padding: 0 8px 0 0; flex-shrink: 0; overflow: hidden; }
|
|
3482
|
+
#proj-tab-list { display: flex; align-items: center; min-width: 0; flex: 1 1 auto; overflow-x: auto; overflow-y: hidden; white-space: nowrap; }
|
|
3456
3483
|
.ptab { padding: 9px 16px; font-size: 13px; font-weight: 500; color: var(--muted); border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; flex-shrink: 0; margin-bottom: -1px; }
|
|
3457
3484
|
.ptab.on { color: var(--text); border-bottom-color: var(--text); font-weight: 600; }
|
|
3458
3485
|
.ptab:hover { color: var(--text); }
|
|
@@ -4206,18 +4233,18 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4206
4233
|
.modal-hdr { padding: 18px 22px; border-bottom: 1px solid var(--line); }
|
|
4207
4234
|
.modal-hdr h2 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
|
|
4208
4235
|
.modal-hdr p { font-size: 13px; color: var(--muted); margin: 0; line-height: 1.5; }
|
|
4209
|
-
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4210
|
-
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4211
|
-
.modal-close:hover { color: var(--text); }
|
|
4212
|
-
.modal-close:focus, .modal-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4213
|
-
.aom-cancel-btn { background: var(--surface); border: 1px solid color-mix(in srgb, var(--accent-strong) 52%, var(--line)); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--accent-strong); }
|
|
4214
|
-
.aom-cancel-btn:hover { border-color: var(--accent-strong); color: var(--accent-strong); }
|
|
4215
|
-
.aom-cancel-btn:focus, .aom-cancel-btn:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4216
|
-
.modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
|
|
4217
|
-
.modal-next { padding: 9px 22px; background: var(--accent-strong); color: var(--bg); border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
4218
|
-
.modal-next:hover { opacity: .9; }
|
|
4219
|
-
.modal-next:focus, .modal-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4220
|
-
.modal-back { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
|
|
4236
|
+
.modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
4237
|
+
.modal-close { float: right; background: var(--bg); border: none; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; cursor: pointer; color: var(--muted); line-height: 1; }
|
|
4238
|
+
.modal-close:hover { color: var(--text); }
|
|
4239
|
+
.modal-close:focus, .modal-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4240
|
+
.aom-cancel-btn { background: var(--surface); border: 1px solid color-mix(in srgb, var(--accent-strong) 52%, var(--line)); border-radius: 20px; padding: 9px 20px; font-size: 14px; cursor: pointer; color: var(--accent-strong); }
|
|
4241
|
+
.aom-cancel-btn:hover { border-color: var(--accent-strong); color: var(--accent-strong); }
|
|
4242
|
+
.aom-cancel-btn:focus, .aom-cancel-btn:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4243
|
+
.modal-footer { padding: 12px 22px 20px; display: flex; align-items: center; justify-content: space-between; }
|
|
4244
|
+
.modal-next { padding: 9px 22px; background: var(--accent-strong); color: var(--bg); border: none; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
4245
|
+
.modal-next:hover { opacity: .9; }
|
|
4246
|
+
.modal-next:focus, .modal-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4247
|
+
.modal-back { font-size: 13px; color: var(--muted); background: none; border: none; cursor: pointer; }
|
|
4221
4248
|
.modal-step-dots { display: flex; gap: 6px; justify-content: center; margin-bottom: 18px; }
|
|
4222
4249
|
.modal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
|
|
4223
4250
|
.modal-dot.on { background: var(--text); }
|
|
@@ -4674,8 +4701,8 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4674
4701
|
align-items: center;
|
|
4675
4702
|
justify-content: center;
|
|
4676
4703
|
}
|
|
4677
|
-
.np-close:hover { color: var(--text); }
|
|
4678
|
-
.np-close:focus, .np-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4704
|
+
.np-close:hover { color: var(--text); }
|
|
4705
|
+
.np-close:focus, .np-close:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4679
4706
|
|
|
4680
4707
|
.np-step-dots {
|
|
4681
4708
|
display: flex;
|
|
@@ -4737,29 +4764,29 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4737
4764
|
justify-content: space-between;
|
|
4738
4765
|
}
|
|
4739
4766
|
|
|
4740
|
-
.np-next {
|
|
4741
|
-
padding: 9px 22px;
|
|
4742
|
-
background: var(--accent-strong);
|
|
4743
|
-
color: var(--bg);
|
|
4744
|
-
border: none;
|
|
4745
|
-
border-radius: 20px;
|
|
4746
|
-
font-size: 14px;
|
|
4747
|
-
font-weight: 600;
|
|
4748
|
-
cursor: pointer;
|
|
4749
|
-
}
|
|
4750
|
-
.np-next:hover { opacity: .88; }
|
|
4751
|
-
.np-next:focus, .np-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4752
|
-
.np-next:disabled { opacity: .55; cursor: not-allowed; }
|
|
4753
|
-
|
|
4754
|
-
.np-back {
|
|
4755
|
-
font-size: 13px;
|
|
4756
|
-
color: var(--accent-strong);
|
|
4757
|
-
background: none;
|
|
4758
|
-
border: none;
|
|
4759
|
-
cursor: pointer;
|
|
4760
|
-
}
|
|
4761
|
-
.np-back:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
4762
|
-
.np-back:focus, .np-back:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4767
|
+
.np-next {
|
|
4768
|
+
padding: 9px 22px;
|
|
4769
|
+
background: var(--accent-strong);
|
|
4770
|
+
color: var(--bg);
|
|
4771
|
+
border: none;
|
|
4772
|
+
border-radius: 20px;
|
|
4773
|
+
font-size: 14px;
|
|
4774
|
+
font-weight: 600;
|
|
4775
|
+
cursor: pointer;
|
|
4776
|
+
}
|
|
4777
|
+
.np-next:hover { opacity: .88; }
|
|
4778
|
+
.np-next:focus, .np-next:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4779
|
+
.np-next:disabled { opacity: .55; cursor: not-allowed; }
|
|
4780
|
+
|
|
4781
|
+
.np-back {
|
|
4782
|
+
font-size: 13px;
|
|
4783
|
+
color: var(--accent-strong);
|
|
4784
|
+
background: none;
|
|
4785
|
+
border: none;
|
|
4786
|
+
cursor: pointer;
|
|
4787
|
+
}
|
|
4788
|
+
.np-back:hover { color: var(--accent-strong); text-decoration: underline; }
|
|
4789
|
+
.np-back:focus, .np-back:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4763
4790
|
|
|
4764
4791
|
/* ── Discard-guard confirm (shown when a dirty wizard is dismissed) ── */
|
|
4765
4792
|
.np-discard {
|
|
@@ -4784,11 +4811,11 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4784
4811
|
.np-discard-msg { font-size: 13px; color: var(--muted); margin: 0 0 18px; line-height: 1.5; }
|
|
4785
4812
|
.np-discard-row { display: flex; gap: 8px; justify-content: flex-end; }
|
|
4786
4813
|
.np-discard-row button { font: inherit; font-size: 13px; font-weight: 600; border-radius: 9px; padding: 8px 16px; cursor: pointer; }
|
|
4787
|
-
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4788
|
-
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4789
|
-
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4790
|
-
.np-discard-yes:hover { opacity: .9; }
|
|
4791
|
-
.np-discard-row button:focus, .np-discard-row button:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4814
|
+
.np-discard-keep { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
|
|
4815
|
+
.np-discard-keep:hover { border-color: var(--muted); }
|
|
4816
|
+
.np-discard-yes { background: var(--danger, #d2261f); color: #fff; border: none; }
|
|
4817
|
+
.np-discard-yes:hover { opacity: .9; }
|
|
4818
|
+
.np-discard-row button:focus, .np-discard-row button:focus-visible { outline: 2px solid var(--accent-strong); outline-offset: 2px; }
|
|
4792
4819
|
|
|
4793
4820
|
/* ── D11: Source attachment row (step 2) ── */
|
|
4794
4821
|
.np-source {
|
|
@@ -5275,9 +5302,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
5275
5302
|
/* ── New-project: folder picker row ── */
|
|
5276
5303
|
.np-folder-row { display:flex; gap:8px; }
|
|
5277
5304
|
.np-folder-row input { flex:1; }
|
|
5278
|
-
.np-browse-btn { padding:9px 16px; background:var(--accent-strong); color:var(--bg); border:none; border-radius:9px; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
|
5279
|
-
.np-browse-btn:hover { opacity:.85; }
|
|
5280
|
-
.np-browse-btn:focus, .np-browse-btn:focus-visible { outline:2px solid var(--accent-strong); outline-offset:2px; }
|
|
5305
|
+
.np-browse-btn { padding:9px 16px; background:var(--accent-strong); color:var(--bg); border:none; border-radius:9px; font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
|
5306
|
+
.np-browse-btn:hover { opacity:.85; }
|
|
5307
|
+
.np-browse-btn:focus, .np-browse-btn:focus-visible { outline:2px solid var(--accent-strong); outline-offset:2px; }
|
|
5281
5308
|
.np-field-hint { font-size:11px; color:var(--muted); margin-top:5px; line-height:1.5; }
|
|
5282
5309
|
|
|
5283
5310
|
@media (max-width: 520px) {
|