fraim-hub 2.0.267 → 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 +119 -9
- package/dist/src/ai-hub/ui-runtime.js +5 -34
- package/package.json +2 -2
- package/public/ai-hub/script.js +37 -11
- 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;
|
|
@@ -200,6 +208,9 @@ function resolveConversationPersonaKey(conversation, projectPath, customJobOwner
|
|
|
200
208
|
const custom = customJobOwners
|
|
201
209
|
? customJobOwners.get(jobId) ?? null
|
|
202
210
|
: getCustomPersonaForJob(projectPath, jobId);
|
|
211
|
+
if (!custom && conversation.personaKey?.startsWith('custom:')) {
|
|
212
|
+
return conversation.personaKey;
|
|
213
|
+
}
|
|
203
214
|
return custom ?? getHubPersonaForJob(jobId);
|
|
204
215
|
}
|
|
205
216
|
/**
|
|
@@ -953,6 +964,28 @@ function normalizePersistedPhaseHistory(value) {
|
|
|
953
964
|
})
|
|
954
965
|
.filter((entry) => Boolean(entry));
|
|
955
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
|
+
}
|
|
956
989
|
function readPersistedRunProjection(conversation) {
|
|
957
990
|
const rawRun = conversation?.run;
|
|
958
991
|
if (!rawRun || typeof rawRun !== 'object')
|
|
@@ -961,6 +994,7 @@ function readPersistedRunProjection(conversation) {
|
|
|
961
994
|
return {
|
|
962
995
|
currentPhase: typeof value.currentPhase === 'string' ? value.currentPhase : null,
|
|
963
996
|
phaseHistory: normalizePersistedPhaseHistory(value.phaseHistory),
|
|
997
|
+
phaseVisits: normalizePersistedPhaseVisits(value.phaseVisits),
|
|
964
998
|
stages: Array.isArray(value.stages) ? value.stages : [],
|
|
965
999
|
totals: value.totals && typeof value.totals === 'object' ? value.totals : null,
|
|
966
1000
|
runDiscriminant: typeof value.runDiscriminant === 'string' ? value.runDiscriminant : null,
|
|
@@ -1137,6 +1171,28 @@ function applyAgentIdentitySignal(run, identity) {
|
|
|
1137
1171
|
run.agentName = identity.agentName;
|
|
1138
1172
|
run.agentModel = identity.agentModel;
|
|
1139
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
|
+
}
|
|
1140
1196
|
function stripStructuredHostPayloads(text) {
|
|
1141
1197
|
return text
|
|
1142
1198
|
.replace(/<delegation_ledger>\s*[\s\S]*?\s*<\/delegation_ledger>/gi, '')
|
|
@@ -1253,7 +1309,25 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
1253
1309
|
};
|
|
1254
1310
|
run.phaseHistory.push(entry);
|
|
1255
1311
|
}
|
|
1312
|
+
recordPhaseVisit(run, signal.phaseId, signal.phaseStatus, signal.findingsText || null);
|
|
1256
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
|
+
}));
|
|
1257
1331
|
}
|
|
1258
1332
|
// Build the stage list for a run. Combines the FSM's reachable path with
|
|
1259
1333
|
// any phases the run has actually visited (in case the run took an
|
|
@@ -1272,7 +1346,8 @@ function deriveStages(run, projectPath) {
|
|
|
1272
1346
|
// phase id NOT in the frontmatter — that last filter prevents
|
|
1273
1347
|
// cross-job pollution from showing up on the tracker.
|
|
1274
1348
|
const allDeclared = (0, catalog_1.loadAllJobPhaseIds)(run.jobId, projectPath);
|
|
1275
|
-
const
|
|
1349
|
+
const visits = phaseVisitsForProjection(run);
|
|
1350
|
+
const visited = visits.map((entry) => entry.phaseId);
|
|
1276
1351
|
const known = new Set(declaredPath.map((p) => p.id));
|
|
1277
1352
|
for (const visitedId of visited) {
|
|
1278
1353
|
if (visitedId === 'starting' || visitedId === '__discriminant__')
|
|
@@ -1288,29 +1363,51 @@ function deriveStages(run, projectPath) {
|
|
|
1288
1363
|
? declaredPath.findIndex((p) => p.id === run.currentPhase)
|
|
1289
1364
|
: -1;
|
|
1290
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
|
+
}
|
|
1291
1372
|
const completedWithoutPhaseTelemetry = run.status === 'completed' &&
|
|
1292
1373
|
currentIndex < 0 &&
|
|
1293
|
-
historyMap.size === 0
|
|
1374
|
+
historyMap.size === 0 &&
|
|
1375
|
+
visits.length === 0;
|
|
1294
1376
|
if (completedWithoutPhaseTelemetry) {
|
|
1295
1377
|
return declaredPath.map((phase) => ({ phaseId: phase.id, label: phase.label, state: 'done' }));
|
|
1296
1378
|
}
|
|
1297
1379
|
return declaredPath.map((phase, index) => {
|
|
1298
1380
|
let state;
|
|
1299
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;
|
|
1300
1386
|
if (index === currentIndex) {
|
|
1301
1387
|
// If the agent has already reported this phase as 'complete', advance
|
|
1302
1388
|
// its visual state to 'done' so the tracker doesn't look frozen while
|
|
1303
1389
|
// waiting for the agent to start the next phase (e.g. after
|
|
1304
1390
|
// implement-submission completes but before address-feedback starts).
|
|
1305
|
-
state =
|
|
1391
|
+
state = latestStatus === 'complete' ? 'done' : 'current';
|
|
1306
1392
|
}
|
|
1307
|
-
else if (
|
|
1393
|
+
else if (latestStatus === 'complete' ||
|
|
1394
|
+
latestStatus === 'failure' ||
|
|
1395
|
+
latestStatus === 'incomplete' ||
|
|
1396
|
+
(currentIndex >= 0 && index < currentIndex && phaseVisits.length > 0)) {
|
|
1308
1397
|
state = 'done';
|
|
1309
1398
|
}
|
|
1310
1399
|
else {
|
|
1311
1400
|
state = 'upcoming';
|
|
1312
1401
|
}
|
|
1313
|
-
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
|
+
};
|
|
1314
1411
|
});
|
|
1315
1412
|
}
|
|
1316
1413
|
// ---------------------------------------------------------------------------
|
|
@@ -2519,6 +2616,7 @@ class AiHubServer {
|
|
|
2519
2616
|
stages,
|
|
2520
2617
|
currentPhase: run.currentPhase || null,
|
|
2521
2618
|
phaseHistory: run.phaseHistory || [],
|
|
2619
|
+
phaseVisits: run.phaseVisits || [],
|
|
2522
2620
|
totals: run.totals || null,
|
|
2523
2621
|
runDiscriminant: run.runDiscriminant || null,
|
|
2524
2622
|
},
|
|
@@ -2788,6 +2886,7 @@ class AiHubServer {
|
|
|
2788
2886
|
],
|
|
2789
2887
|
currentPhase: persistedRun?.currentPhase || null,
|
|
2790
2888
|
phaseHistory: persistedRun?.phaseHistory || [],
|
|
2889
|
+
phaseVisits: persistedRun?.phaseVisits || [],
|
|
2791
2890
|
totals: persistedRun?.totals || emptyTotals(),
|
|
2792
2891
|
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
2793
2892
|
artifacts: Array.isArray(conversation.artifacts) ? conversation.artifacts : [],
|
|
@@ -3145,6 +3244,7 @@ class AiHubServer {
|
|
|
3145
3244
|
events: [(0, hosts_1.createHubEvent)('system', `Mandy started delegated workstream ${task.taskId} for ${task.personaKey || task.jobId}.`)],
|
|
3146
3245
|
currentPhase: null,
|
|
3147
3246
|
phaseHistory: [],
|
|
3247
|
+
phaseVisits: [],
|
|
3148
3248
|
totals: emptyTotals(),
|
|
3149
3249
|
lastStatusChangeAt: now,
|
|
3150
3250
|
personaKey: task.personaKey,
|
|
@@ -4148,6 +4248,7 @@ class AiHubServer {
|
|
|
4148
4248
|
],
|
|
4149
4249
|
currentPhase: persistedRun?.currentPhase || null,
|
|
4150
4250
|
phaseHistory: persistedRun?.phaseHistory || [],
|
|
4251
|
+
phaseVisits: persistedRun?.phaseVisits || [],
|
|
4151
4252
|
stages: persistedRun?.stages || [],
|
|
4152
4253
|
totals: persistedRun?.totals || emptyTotals(),
|
|
4153
4254
|
lastStatusChangeAt: now,
|
|
@@ -4420,7 +4521,7 @@ class AiHubServer {
|
|
|
4420
4521
|
this.app.get('/api/ai-hub/version', async (_req, res) => {
|
|
4421
4522
|
const version = (0, version_utils_1.getFraimVersion)();
|
|
4422
4523
|
const latest = await (0, hub_latest_version_1.getLatestPublishedVersion)();
|
|
4423
|
-
const updateAvailable =
|
|
4524
|
+
const updateAvailable = isNewerPublishedVersion(latest, version);
|
|
4424
4525
|
const uiRuntime = await this.resolveHubUiRuntimeResponse();
|
|
4425
4526
|
return res.json({
|
|
4426
4527
|
version,
|
|
@@ -4585,7 +4686,12 @@ class AiHubServer {
|
|
|
4585
4686
|
return res.json({ ok: true, brand: stored, published: false });
|
|
4586
4687
|
}
|
|
4587
4688
|
try {
|
|
4588
|
-
|
|
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([{
|
|
4589
4695
|
relativePath: 'context/org_brand.json',
|
|
4590
4696
|
content: JSON.stringify(stored, null, 2),
|
|
4591
4697
|
}]);
|
|
@@ -4854,6 +4960,7 @@ class AiHubServer {
|
|
|
4854
4960
|
// Issue #347 — seed phase + totals state on creation.
|
|
4855
4961
|
currentPhase: null,
|
|
4856
4962
|
phaseHistory: [],
|
|
4963
|
+
phaseVisits: [],
|
|
4857
4964
|
totals: emptyTotals(),
|
|
4858
4965
|
lastStatusChangeAt: startTimestamp,
|
|
4859
4966
|
personaKey: jobMetadata?.personaKey ?? getHubPersonaForJob(jobId),
|
|
@@ -5285,6 +5392,7 @@ class AiHubServer {
|
|
|
5285
5392
|
// resuming the session starts a fresh job, so the tracker must be blank.
|
|
5286
5393
|
currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
|
|
5287
5394
|
phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
|
|
5395
|
+
phaseVisits: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseVisits || []) : [],
|
|
5288
5396
|
totals: persistedRun?.totals || emptyTotals(),
|
|
5289
5397
|
lastStatusChangeAt: now,
|
|
5290
5398
|
runDiscriminant: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
|
|
@@ -5962,6 +6070,7 @@ class AiHubServer {
|
|
|
5962
6070
|
events: [(0, hosts_1.createHubEvent)('system', `Trigger: ${configuredAgent.label} / ${jobName} from ${context?.sourceApp || 'unknown'} in ${projectPath}`)],
|
|
5963
6071
|
currentPhase: null,
|
|
5964
6072
|
phaseHistory: [],
|
|
6073
|
+
phaseVisits: [],
|
|
5965
6074
|
totals: emptyTotals(),
|
|
5966
6075
|
lastStatusChangeAt: startTimestamp,
|
|
5967
6076
|
personaKey: getHubPersonaForJob(jobName),
|
|
@@ -6113,6 +6222,7 @@ class AiHubServer {
|
|
|
6113
6222
|
events: [(0, hosts_1.createHubEvent)('system', `Triggered by deployment: ${deployment.label} (${deployment.type}) using ${configuredAgent.label}`)],
|
|
6114
6223
|
currentPhase: null,
|
|
6115
6224
|
phaseHistory: [],
|
|
6225
|
+
phaseVisits: [],
|
|
6116
6226
|
totals: emptyTotals(),
|
|
6117
6227
|
lastStatusChangeAt: startTimestamp,
|
|
6118
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
|
@@ -765,6 +765,12 @@ function ensureTaskPaneLauncher() {
|
|
|
765
765
|
const project = taskPaneProjectEntries().find((entry) => entry.folderPath === event.target.value);
|
|
766
766
|
if (statusEl && project) statusEl.textContent = `Project: ${project.name || friendlyProjectShortName(project.folderPath)}`;
|
|
767
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
|
+
});
|
|
768
774
|
return root;
|
|
769
775
|
}
|
|
770
776
|
|
|
@@ -855,6 +861,8 @@ async function taskPaneStartSelectedJob() {
|
|
|
855
861
|
if (result && result.ok) {
|
|
856
862
|
taskPaneLauncherForcedOpen = false;
|
|
857
863
|
if (statusEl) statusEl.textContent = 'Started.';
|
|
864
|
+
renderActive();
|
|
865
|
+
syncTaskPaneSurfaceMode();
|
|
858
866
|
} else if (statusEl) {
|
|
859
867
|
statusEl.textContent = (result && result.error) || 'Could not start.';
|
|
860
868
|
}
|
|
@@ -863,6 +871,7 @@ async function taskPaneStartSelectedJob() {
|
|
|
863
871
|
console.warn('[ai-hub] task-pane start failed:', error);
|
|
864
872
|
} finally {
|
|
865
873
|
renderTaskPaneLauncher();
|
|
874
|
+
syncTaskPaneSurfaceMode();
|
|
866
875
|
}
|
|
867
876
|
}
|
|
868
877
|
|
|
@@ -1028,18 +1037,18 @@ function normalizeGeminiConversationMessages(conv) {
|
|
|
1028
1037
|
// request 413s, the error is swallowed, and the client-owned fields in the SAME
|
|
1029
1038
|
// payload (e.g. pauseReason='done' from "Mark complete") never persist. Strip them
|
|
1030
1039
|
// here; the server's PUT handler merges them back from the stored record.
|
|
1031
|
-
const SERVER_OWNED_CONV_FIELDS = ['messages', 'events', 'artifacts', 'run', 'delegation', 'handoffSummary'];
|
|
1032
|
-
// Issue #1090: `_bodyFetched` records that THIS CLIENT successfully fetched the
|
|
1033
|
-
// body, which is a different claim from "the record has body fields". A run can
|
|
1034
|
-
// 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;
|
|
1035
1044
|
// before this marker existed such a body was indistinguishable from one that had
|
|
1036
1045
|
// never been hydrated, so the transcript hung on "Loading conversation…" and
|
|
1037
1046
|
// re-fetched on every poll tick forever.
|
|
1038
1047
|
//
|
|
1039
|
-
// It is listed in CLIENT_ONLY_CONV_FIELDS so `cleanConversationHeader()` strips
|
|
1040
|
-
// any inbound value. That is what preserves issue #913: a marker arriving over
|
|
1041
|
-
// the wire is never trusted, only one this client set after a real fetch.
|
|
1042
|
-
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'];
|
|
1043
1052
|
function slimConversationForPersist(conv) {
|
|
1044
1053
|
if (!conv || typeof conv !== 'object') return conv;
|
|
1045
1054
|
const slim = { ...conv };
|
|
@@ -3905,7 +3914,7 @@ function renderTracker(conv) {
|
|
|
3905
3914
|
const rowCapacity = trackerRowCapacity(tracker, stages);
|
|
3906
3915
|
|
|
3907
3916
|
// Cache key so we only rebuild the DOM when the stage data changes.
|
|
3908
|
-
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('|') +
|
|
3909
3918
|
'#' + (conv.run.currentPhase || '') +
|
|
3910
3919
|
'#' + rowCapacity;
|
|
3911
3920
|
if (key === renderedTrackerKey) {
|
|
@@ -3927,7 +3936,9 @@ function renderTracker(conv) {
|
|
|
3927
3936
|
rowsHost.parentNode.insertBefore(activeLabelHost, els['tracker-note']);
|
|
3928
3937
|
}
|
|
3929
3938
|
const currentStage = stages.find((s) => s.state === 'current');
|
|
3930
|
-
activeLabelHost.textContent = currentStage
|
|
3939
|
+
activeLabelHost.textContent = currentStage
|
|
3940
|
+
? `${currentStage.label}${currentStage.currentVisit && currentStage.currentVisit > 1 ? ` - visit ${currentStage.currentVisit}` : ''}`
|
|
3941
|
+
: '';
|
|
3931
3942
|
// The pizza tracker is always a single horizontal row. When the pane is too
|
|
3932
3943
|
// narrow to fit every stage label, collapse to circles + the active-stage
|
|
3933
3944
|
// label (CSS .tracker-compact) instead of wrapping onto stacked rows.
|
|
@@ -3939,12 +3950,21 @@ function renderTracker(conv) {
|
|
|
3939
3950
|
const stageEl = document.createElement('div');
|
|
3940
3951
|
stageEl.className = 'stage ' + stage.state;
|
|
3941
3952
|
const tooltipText = buildStageTooltip(conv, stage);
|
|
3942
|
-
if (tooltipText)
|
|
3953
|
+
if (tooltipText) {
|
|
3954
|
+
stageEl.setAttribute('title', tooltipText);
|
|
3955
|
+
stageEl.setAttribute('aria-label', tooltipText);
|
|
3956
|
+
}
|
|
3943
3957
|
const circle = document.createElement('span');
|
|
3944
3958
|
circle.className = 'stage-circle';
|
|
3945
3959
|
if (stage.state === 'done') circle.textContent = '✓';
|
|
3946
3960
|
else if (stage.state === 'current') circle.textContent = '●';
|
|
3947
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
|
+
}
|
|
3948
3968
|
const label = document.createElement('span');
|
|
3949
3969
|
label.className = 'stage-label';
|
|
3950
3970
|
label.textContent = stage.label;
|
|
@@ -3958,6 +3978,11 @@ function renderTracker(conv) {
|
|
|
3958
3978
|
function buildStageTooltip(conv, stage) {
|
|
3959
3979
|
const history = (conv.run && conv.run.phaseHistory) || [];
|
|
3960
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(' - ');
|
|
3961
3986
|
const findings = entry && entry.latestText;
|
|
3962
3987
|
if (findings) return `${stage.phaseId} · ${findings}`;
|
|
3963
3988
|
return stage.phaseId;
|
|
@@ -7687,6 +7712,7 @@ function foldRunIntoConversation(conv, run) {
|
|
|
7687
7712
|
stages: run.stages || [],
|
|
7688
7713
|
currentPhase: run.currentPhase || null,
|
|
7689
7714
|
phaseHistory: run.phaseHistory || [],
|
|
7715
|
+
phaseVisits: run.phaseVisits || [],
|
|
7690
7716
|
totals: run.totals || null,
|
|
7691
7717
|
runDiscriminant: run.runDiscriminant || null,
|
|
7692
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) {
|