fraim-hub 2.0.348 → 2.0.349
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/hosts.js
CHANGED
|
@@ -2399,11 +2399,14 @@ function detectCodexCompactTaskFailure(line) {
|
|
|
2399
2399
|
return null;
|
|
2400
2400
|
}
|
|
2401
2401
|
function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
2402
|
-
// Exec's JSON error has no auth code.
|
|
2403
|
-
//
|
|
2402
|
+
// Exec's JSON error has no auth code. Refresh failures need corroborating
|
|
2403
|
+
// stderr; workspace-routing 401 needs the exact terminal provider event.
|
|
2404
|
+
// Both require a failed process exit; never scan tool/prose text.
|
|
2404
2405
|
const codexRefreshFailure = 'Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.';
|
|
2406
|
+
const codexWorkspaceRoutingFailure = 'workspace routing discovery unauthorized (401)';
|
|
2405
2407
|
let codexAuthDiagnostic = false;
|
|
2406
2408
|
let codexTerminalAuthFailure = false;
|
|
2409
|
+
let codexTerminalWorkspaceAuthFailure = false;
|
|
2407
2410
|
const observeCodexAuth = (line, channel) => {
|
|
2408
2411
|
if (hostId !== 'codex')
|
|
2409
2412
|
return;
|
|
@@ -2416,11 +2419,15 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2416
2419
|
}
|
|
2417
2420
|
try {
|
|
2418
2421
|
const event = JSON.parse(line);
|
|
2422
|
+
if (event.type === 'turn.failed') {
|
|
2423
|
+
codexTerminalWorkspaceAuthFailure = event.error?.message === codexWorkspaceRoutingFailure;
|
|
2424
|
+
}
|
|
2419
2425
|
if (event.type === 'turn.failed' || event.type === 'error') {
|
|
2420
2426
|
codexTerminalAuthFailure = (event.type === 'turn.failed' ? event.error?.message : event.message) === codexRefreshFailure;
|
|
2421
2427
|
}
|
|
2422
2428
|
else if (event.type === 'turn.started' || event.type === 'turn.completed') {
|
|
2423
2429
|
codexTerminalAuthFailure = false;
|
|
2430
|
+
codexTerminalWorkspaceAuthFailure = false;
|
|
2424
2431
|
}
|
|
2425
2432
|
}
|
|
2426
2433
|
catch { /* Plain stdout cannot establish authentication failure. */ }
|
|
@@ -2474,8 +2481,9 @@ function wireHostProcess(hostId, child, handlers, spawnCwd) {
|
|
|
2474
2481
|
// `exitCode ?? 0` guard. Pass -1 for any signal-terminated child so the
|
|
2475
2482
|
// non-zero branch fires and recovery is attempted.
|
|
2476
2483
|
child.on('close', (code, signal) => {
|
|
2477
|
-
if (!signal && code !== null && code !== 0 && codexAuthDiagnostic && codexTerminalAuthFailure) {
|
|
2478
|
-
|
|
2484
|
+
if (!signal && code !== null && code !== 0 && ((codexAuthDiagnostic && codexTerminalAuthFailure) || codexTerminalWorkspaceAuthFailure)) {
|
|
2485
|
+
const failure = codexTerminalWorkspaceAuthFailure ? codexWorkspaceRoutingFailure : codexRefreshFailure;
|
|
2486
|
+
handlers.onEvent({ hostAuthenticationRequired: true, hostError: failure, raw: failure }, 'system');
|
|
2479
2487
|
}
|
|
2480
2488
|
handlers.onExit(signal ? -1 : code);
|
|
2481
2489
|
});
|
|
@@ -50,6 +50,7 @@ class LocalRegistryResolver {
|
|
|
50
50
|
constructor(options) {
|
|
51
51
|
this.workspaceRoot = options.workspaceRoot;
|
|
52
52
|
this.effectiveFraimDir = options.effectiveFraimDir;
|
|
53
|
+
this.userLevelOnly = options.userLevelOnly ?? false;
|
|
53
54
|
this.remoteContentResolver = options.remoteContentResolver;
|
|
54
55
|
this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
|
|
55
56
|
this.shouldFilter = options.shouldFilter;
|
|
@@ -87,7 +88,14 @@ class LocalRegistryResolver {
|
|
|
87
88
|
* Check if a local override exists for the given path
|
|
88
89
|
*/
|
|
89
90
|
hasLocalOverride(path) {
|
|
90
|
-
return (0, fs_1.existsSync)(this.getOverridePath(path));
|
|
91
|
+
return !this.userLevelOnly && (0, fs_1.existsSync)(this.getOverridePath(path));
|
|
92
|
+
}
|
|
93
|
+
/** Whether an effective personalized layer should replace a remote baseline response. */
|
|
94
|
+
hasPersonalizedOverride(path) {
|
|
95
|
+
return this.hasLocalOverride(path)
|
|
96
|
+
|| this.readManagerLocalOverlayFile(path) !== null
|
|
97
|
+
|| this.readManagerOverlayFile(path) !== null
|
|
98
|
+
|| this.readOrgOverlayFile(path) !== null;
|
|
91
99
|
}
|
|
92
100
|
/**
|
|
93
101
|
* Find the actual relative path for a registry item by searching subdirectories.
|
|
@@ -108,11 +116,11 @@ class LocalRegistryResolver {
|
|
|
108
116
|
// `jobs/<category>/<name>.md`, so the job was reported not found even
|
|
109
117
|
// though the file existed.
|
|
110
118
|
const searchRoots = [
|
|
111
|
-
this.getFraimPath('personalized-employee', dir),
|
|
119
|
+
...(!this.userLevelOnly ? [this.getFraimPath('personalized-employee', dir)] : []),
|
|
112
120
|
(0, path_1.join)(this.managerLocalRoot, dir),
|
|
113
121
|
(0, path_1.join)(this.managerCacheRoot, dir),
|
|
114
122
|
(0, path_1.join)(this.orgCacheRoot, dir),
|
|
115
|
-
(0, path_1.join)(this.workspaceRoot, 'registry', dir),
|
|
123
|
+
...(!this.userLevelOnly ? [(0, path_1.join)(this.workspaceRoot, 'registry', dir)] : []),
|
|
116
124
|
// Installed projects discover categorized capabilities in machine catalogs.
|
|
117
125
|
this.getSyncedCatalogRoot('ai-employee', dir),
|
|
118
126
|
...(type !== 'skills' ? [this.getSyncedCatalogRoot('ai-manager', dir)] : []),
|
|
@@ -326,6 +334,8 @@ class LocalRegistryResolver {
|
|
|
326
334
|
}
|
|
327
335
|
}
|
|
328
336
|
readWorkspaceRegistryFile(path) {
|
|
337
|
+
if (this.userLevelOnly)
|
|
338
|
+
return null;
|
|
329
339
|
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
330
340
|
const registryRoot = (0, path_1.join)(this.workspaceRoot, 'registry');
|
|
331
341
|
const registryPath = (0, path_1.join)(registryRoot, ...normalizedPath.split('/'));
|
|
@@ -1535,6 +1535,7 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1535
1535
|
return [];
|
|
1536
1536
|
const out = [];
|
|
1537
1537
|
const seen = new Set();
|
|
1538
|
+
const projectOwnerPath = (0, owner_identity_1.ownerPath)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot));
|
|
1538
1539
|
const headingRe = /^## (Rejected|Superseded): (.+)$/m;
|
|
1539
1540
|
const reasonRe = /^Reason: (.+)$/m;
|
|
1540
1541
|
const revisitRe = /^Revisit when: (.+)$/m;
|
|
@@ -1567,7 +1568,9 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1567
1568
|
let proposal = {};
|
|
1568
1569
|
if (json) {
|
|
1569
1570
|
try {
|
|
1570
|
-
|
|
1571
|
+
const parsed = JSON.parse(json[1]);
|
|
1572
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1573
|
+
proposal = parsed;
|
|
1571
1574
|
}
|
|
1572
1575
|
catch {
|
|
1573
1576
|
// Keep the human-readable half; the JSON only enriches it.
|
|
@@ -1576,12 +1579,24 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1576
1579
|
const decision = (proposal.decision || {});
|
|
1577
1580
|
const content = (proposal.content || {});
|
|
1578
1581
|
const trace = (content.trace || {});
|
|
1582
|
+
let targetOwner;
|
|
1583
|
+
try {
|
|
1584
|
+
const owner = proposal.target?.owner;
|
|
1585
|
+
(0, owner_identity_1.validateOwner)(owner);
|
|
1586
|
+
targetOwner = { level: owner.level, path: owner.path };
|
|
1587
|
+
}
|
|
1588
|
+
catch {
|
|
1589
|
+
// Older/damaged targets remain visible as unknown; never infer
|
|
1590
|
+
// their project from the shared history file's location.
|
|
1591
|
+
}
|
|
1592
|
+
if (targetOwner?.level === 'Project' && targetOwner.path !== projectOwnerPath)
|
|
1593
|
+
continue;
|
|
1579
1594
|
const claimKey = typeof proposal.claimKey === 'string' && proposal.claimKey
|
|
1580
1595
|
? proposal.claimKey
|
|
1581
1596
|
: parts[1] || heading[2];
|
|
1582
1597
|
// The same claim can appear in more than one configured base. The first
|
|
1583
1598
|
// base supplies display metadata for that same recorded decision.
|
|
1584
|
-
const key =
|
|
1599
|
+
const key = JSON.stringify([claimKey, String(decision.at || ''), targetOwner?.level, targetOwner?.path]);
|
|
1585
1600
|
if (seen.has(key))
|
|
1586
1601
|
continue;
|
|
1587
1602
|
seen.add(key);
|
|
@@ -1596,6 +1611,7 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1596
1611
|
rejectedAt: typeof decision.at === 'string' ? decision.at : '',
|
|
1597
1612
|
...(typeof trace.recurrences === 'number' ? { recurrencesAtRejection: trace.recurrences } : {}),
|
|
1598
1613
|
displayPath,
|
|
1614
|
+
...(targetOwner ? { targetOwner } : {}),
|
|
1599
1615
|
});
|
|
1600
1616
|
}
|
|
1601
1617
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.349",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"author": "Sid Mathur <sid.mathur@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/mathursrus/FRAIM#readme",
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
"electron-updater": "^6.8.9",
|
|
247
247
|
"express": "^5.2.1",
|
|
248
248
|
"extract-zip": "^2.0.1",
|
|
249
|
-
"fraim": "2.0.
|
|
249
|
+
"fraim": "2.0.349",
|
|
250
250
|
"mongodb": "^7.0.0",
|
|
251
251
|
"node-cron": "4.2.1",
|
|
252
252
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/review.css
CHANGED
|
@@ -833,6 +833,13 @@
|
|
|
833
833
|
}
|
|
834
834
|
|
|
835
835
|
/* Card body clamp */
|
|
836
|
+
.learn-prop .lr-owner {
|
|
837
|
+
min-width: 0;
|
|
838
|
+
max-width: 100%;
|
|
839
|
+
flex-shrink: 1;
|
|
840
|
+
overflow-wrap: anywhere;
|
|
841
|
+
}
|
|
842
|
+
|
|
836
843
|
.learn-prop .lp-body.lp-clamped {
|
|
837
844
|
display: -webkit-box;
|
|
838
845
|
-webkit-line-clamp: 3;
|
package/public/ai-hub/script.js
CHANGED
|
@@ -7346,85 +7346,9 @@ function renderLearningProposalTurn(conv) {
|
|
|
7346
7346
|
const host = els['messages'];
|
|
7347
7347
|
if (!host) return;
|
|
7348
7348
|
renderLearningProposalHistory(conv);
|
|
7349
|
+
// Issue #2045: sleep conversational cards disabled — not working as expected.
|
|
7349
7350
|
const existing = host.querySelector('#learn-proposal-turn');
|
|
7350
|
-
if (!tfConvHasProposals(conv)) {
|
|
7351
|
-
if (existing) existing.remove();
|
|
7352
|
-
return;
|
|
7353
|
-
}
|
|
7354
|
-
|
|
7355
|
-
// Issue #1857: the scope arrives on the set rather than being re-derived from
|
|
7356
|
-
// the kickoff text, so a decision reaches the corpus its run was scoped to even
|
|
7357
|
-
// when the kickoff wording is ambiguous.
|
|
7358
|
-
const scope = conv.proposalSet.runScope || tfProposalScopeForConversation(conv);
|
|
7359
|
-
const proposals = tfPendingProposals(conv);
|
|
7360
|
-
if (!proposals.length) {
|
|
7361
|
-
if (existing) existing.remove();
|
|
7362
|
-
return;
|
|
7363
|
-
}
|
|
7364
|
-
const signature = JSON.stringify([conv.id, tfProposalSubmissions.has(conv.id), conv.proposalSet, proposals.map((p) => {
|
|
7365
|
-
const key = tfProposalStateKey(p);
|
|
7366
|
-
const override = tfProposalOverrides[key] || {};
|
|
7367
|
-
return [tfProposalDecisions[key], override.level, override.skipped];
|
|
7368
|
-
})]);
|
|
7369
|
-
if (existing && existing.proposalSignature === signature) return;
|
|
7370
7351
|
if (existing) existing.remove();
|
|
7371
|
-
|
|
7372
|
-
const row = document.createElement('article');
|
|
7373
|
-
row.className = 'message employee review-turn learn-proposal-turn';
|
|
7374
|
-
row.id = 'learn-proposal-turn';
|
|
7375
|
-
row.dataset.testid = 'learn-proposal-turn';
|
|
7376
|
-
row.proposalSignature = signature;
|
|
7377
|
-
const bubble = document.createElement('div');
|
|
7378
|
-
bubble.className = 'bubble';
|
|
7379
|
-
|
|
7380
|
-
const label = document.createElement('div');
|
|
7381
|
-
label.className = 'review-turn-label';
|
|
7382
|
-
const undecided = proposals.filter((p) => !tfProposalDecisions[tfProposalStateKey(p)]).length;
|
|
7383
|
-
label.textContent = undecided
|
|
7384
|
-
? `${undecided} lesson${undecided === 1 ? '' : 's'} waiting on your decision`
|
|
7385
|
-
: 'Lessons reviewed';
|
|
7386
|
-
bubble.appendChild(label);
|
|
7387
|
-
|
|
7388
|
-
if (undecided) bubble.appendChild(tfLearningBatchBar(proposals, scope));
|
|
7389
|
-
const list = document.createElement('div');
|
|
7390
|
-
list.className = 'learn-list';
|
|
7391
|
-
for (const p of proposals) list.appendChild(tfProposalCard(p, scope));
|
|
7392
|
-
bubble.appendChild(list);
|
|
7393
|
-
|
|
7394
|
-
// Issue #1857 R7. A claim the manager already decided is not a card, because
|
|
7395
|
-
// re-asking a settled question is the cost this issue exists to remove. It is
|
|
7396
|
-
// also not silence: suppression the manager cannot see is a system quietly
|
|
7397
|
-
// deciding on their behalf. One line each, after the batch.
|
|
7398
|
-
const suppressed = tfSuppressedClaims(conv);
|
|
7399
|
-
if (suppressed.length) {
|
|
7400
|
-
const note = document.createElement('div');
|
|
7401
|
-
note.className = 'learn-suppressed';
|
|
7402
|
-
note.dataset.testid = 'learn-suppressed';
|
|
7403
|
-
const lead = document.createElement('div');
|
|
7404
|
-
lead.className = 'ls-lead';
|
|
7405
|
-
lead.textContent = `Not shown: ${suppressed.length} claim${suppressed.length === 1 ? '' : 's'} `
|
|
7406
|
-
+ `matched ${suppressed.length === 1 ? 'a decision' : 'decisions'} you already made.`;
|
|
7407
|
-
note.appendChild(lead);
|
|
7408
|
-
for (const claim of suppressed) {
|
|
7409
|
-
const line = document.createElement('div');
|
|
7410
|
-
line.className = 'ls-row';
|
|
7411
|
-
// D2: a claim a rule or skill now owns was promoted, not refused. Saying
|
|
7412
|
-
// "you rejected this" about work the manager approved elsewhere is wrong.
|
|
7413
|
-
line.textContent = claim.kind === 'superseded'
|
|
7414
|
-
? `"${claim.claimKey}" superseded ${claim.rejectedAt}: ${claim.reason}`
|
|
7415
|
-
: `"${claim.claimKey}" rejected ${claim.rejectedAt}: "${claim.reason}"`
|
|
7416
|
-
+ (claim.newSourceCount ? ` - ${claim.newSourceCount} new source${claim.newSourceCount === 1 ? '' : 's'}` : '');
|
|
7417
|
-
note.appendChild(line);
|
|
7418
|
-
}
|
|
7419
|
-
bubble.appendChild(note);
|
|
7420
|
-
}
|
|
7421
|
-
|
|
7422
|
-
row.appendChild(bubble);
|
|
7423
|
-
if (tfProposalSubmissions.has(conv.id)) {
|
|
7424
|
-
row.querySelectorAll('button, textarea').forEach((control) => { control.disabled = true; });
|
|
7425
|
-
}
|
|
7426
|
-
// Below the thread, above the deliverables/next-job furniture.
|
|
7427
|
-
host.insertBefore(row, host.querySelector('#deliverables-bar, #next-job-section'));
|
|
7428
7352
|
}
|
|
7429
7353
|
|
|
7430
7354
|
// Like ordinary reviewTurns, acknowledged proposals belong to conversation history.
|
|
@@ -15389,17 +15313,42 @@ async function tfFetchPreservedLearnings(scope, level) {
|
|
|
15389
15313
|
* `undefined` = not loaded yet, so a first render can say "Loading" rather than
|
|
15390
15314
|
* claiming there are no rejections.
|
|
15391
15315
|
*/
|
|
15392
|
-
|
|
15316
|
+
let tfRejectionsGeneration = 0;
|
|
15317
|
+
function tfRejectionsCache() {
|
|
15318
|
+
return state._rejectionsProject === tfCanonicalProjectPath(state.projectPath || '') ? state._rejections : undefined;
|
|
15319
|
+
}
|
|
15320
|
+
function tfInvalidateRejections() {
|
|
15321
|
+
tfRejectionsGeneration += 1;
|
|
15322
|
+
state._rejections = undefined;
|
|
15323
|
+
state._rejectionsPending = undefined;
|
|
15324
|
+
state._rejectionsProject = undefined;
|
|
15325
|
+
}
|
|
15393
15326
|
async function tfFetchRejections() {
|
|
15327
|
+
const projectPath = state.projectPath || '';
|
|
15328
|
+
const projectKey = tfCanonicalProjectPath(projectPath);
|
|
15329
|
+
if (state._rejectionsPending?.projectKey === projectKey) return state._rejectionsPending.promise;
|
|
15330
|
+
const generation = ++tfRejectionsGeneration;
|
|
15394
15331
|
const params = new URLSearchParams();
|
|
15395
|
-
if (
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15332
|
+
if (projectPath) params.set('projectPath', projectPath);
|
|
15333
|
+
const promise = (async () => {
|
|
15334
|
+
let entries;
|
|
15335
|
+
try {
|
|
15336
|
+
const data = await requestJson('/api/ai-hub/rejections?' + params.toString());
|
|
15337
|
+
entries = Array.isArray(data.entries) ? data.entries : [];
|
|
15338
|
+
} catch {
|
|
15339
|
+
entries = [];
|
|
15340
|
+
}
|
|
15341
|
+
// Both identity and generation matter: an A -> B -> A switch must not
|
|
15342
|
+
// resurrect a response from the first visit to A.
|
|
15343
|
+
if (generation === tfRejectionsGeneration && projectKey === tfCanonicalProjectPath(state.projectPath || '')) {
|
|
15344
|
+
state._rejections = entries;
|
|
15345
|
+
state._rejectionsProject = projectKey;
|
|
15346
|
+
state._rejectionsPending = undefined;
|
|
15347
|
+
}
|
|
15348
|
+
return entries;
|
|
15349
|
+
})();
|
|
15350
|
+
state._rejectionsPending = { projectKey, promise };
|
|
15351
|
+
return promise;
|
|
15403
15352
|
}
|
|
15404
15353
|
|
|
15405
15354
|
/**
|
|
@@ -15408,13 +15357,13 @@ async function tfFetchRejections() {
|
|
|
15408
15357
|
* because a refused claim is settled and the library is about what the employee
|
|
15409
15358
|
* knows, not what it was told no about.
|
|
15410
15359
|
*/
|
|
15411
|
-
function tfRejectedSection(rejections) {
|
|
15360
|
+
function tfRejectedSection(rejections, unknownScope = false) {
|
|
15412
15361
|
const box = document.createElement('details');
|
|
15413
15362
|
box.className = 'learn-dormant-section learn-rejected-section';
|
|
15414
|
-
box.dataset.testid = 'rejected-section';
|
|
15363
|
+
box.dataset.testid = unknownScope ? 'unknown-rejection-section' : 'rejected-section';
|
|
15415
15364
|
const summary = document.createElement('summary');
|
|
15416
15365
|
summary.className = 'learn-dormant-header';
|
|
15417
|
-
summary.textContent =
|
|
15366
|
+
summary.textContent = `${unknownScope ? 'History with unknown scope' : 'Rejected'} (${rejections.length})`;
|
|
15418
15367
|
box.appendChild(summary);
|
|
15419
15368
|
|
|
15420
15369
|
for (const r of rejections) {
|
|
@@ -15445,6 +15394,14 @@ function tfRejectedSection(rejections) {
|
|
|
15445
15394
|
|
|
15446
15395
|
const meta = document.createElement('div');
|
|
15447
15396
|
meta.className = 'lp-stats';
|
|
15397
|
+
const owner = document.createElement('span');
|
|
15398
|
+
owner.className = 'lp-score lr-owner';
|
|
15399
|
+
owner.dataset.testid = 'lr-owner';
|
|
15400
|
+
const targetOwner = r.targetOwner;
|
|
15401
|
+
owner.textContent = targetOwner
|
|
15402
|
+
? `${targetOwner.level === 'Org' ? 'Company' : targetOwner.level}: ${targetOwner.path}`
|
|
15403
|
+
: 'Scope unknown';
|
|
15404
|
+
meta.appendChild(owner);
|
|
15448
15405
|
if (r.rejectedAt) {
|
|
15449
15406
|
const when = document.createElement('span');
|
|
15450
15407
|
when.className = 'lp-score';
|
|
@@ -16096,14 +16053,23 @@ function tfRenderLearningsList(host, scope, level, emptyText) {
|
|
|
16096
16053
|
if (preserved === undefined) return;
|
|
16097
16054
|
}
|
|
16098
16055
|
|
|
16099
|
-
//
|
|
16100
|
-
// surface
|
|
16101
|
-
//
|
|
16056
|
+
// The reader scopes Project targets to the request's project. Each Info
|
|
16057
|
+
// surface then selects its own recorded level from that project-keyed cache.
|
|
16058
|
+
// Org history delivery is separate from these personal/project surfaces.
|
|
16102
16059
|
const showsRejections = scope === 'manager';
|
|
16103
16060
|
if (showsRejections && tfRejectionsCache() === undefined) {
|
|
16104
16061
|
tfFetchRejections().then(() => tfReRenderScope(scope, level)).catch(() => {});
|
|
16105
16062
|
}
|
|
16106
|
-
const
|
|
16063
|
+
const history = showsRejections && Array.isArray(tfRejectionsCache()) ? tfRejectionsCache() : [];
|
|
16064
|
+
const ownerLevel = level === 'project' ? 'Project' : level === 'machine' ? 'Manager' : undefined;
|
|
16065
|
+
const rejections = history.filter(entry => ownerLevel && entry.targetOwner?.level === ownerLevel);
|
|
16066
|
+
// Unknown ownership has one inspection home, separate from Manager's owned
|
|
16067
|
+
// rejection count. Keeping it visible does not assign it Manager ownership.
|
|
16068
|
+
const unknownHistory = level === 'machine' ? history.filter(entry => !entry.targetOwner) : [];
|
|
16069
|
+
const appendRejectionHistory = () => {
|
|
16070
|
+
if (rejections.length) host.appendChild(tfRejectedSection(rejections));
|
|
16071
|
+
if (unknownHistory.length) host.appendChild(tfRejectedSection(unknownHistory, true));
|
|
16072
|
+
};
|
|
16107
16073
|
|
|
16108
16074
|
const addCategories = scope === 'reverse' ? ['coaching']
|
|
16109
16075
|
: scope === 'org' ? ['avoid', 'preference', 'repeat', 'coaching']
|
|
@@ -16209,7 +16175,7 @@ function tfRenderLearningsList(host, scope, level, emptyText) {
|
|
|
16209
16175
|
|
|
16210
16176
|
if (list.children.length) {
|
|
16211
16177
|
host.appendChild(list);
|
|
16212
|
-
if (
|
|
16178
|
+
if (!shareMode) appendRejectionHistory();
|
|
16213
16179
|
if (!shareMode) { host.appendChild(addBtn); wireAdd(); }
|
|
16214
16180
|
if (shareMode) tfUpdateShareBar(host, scope, level);
|
|
16215
16181
|
return;
|
|
@@ -16232,7 +16198,7 @@ function tfRenderLearningsList(host, scope, level, emptyText) {
|
|
|
16232
16198
|
empty.appendChild(emoji); empty.appendChild(title); empty.appendChild(sub);
|
|
16233
16199
|
host.appendChild(empty);
|
|
16234
16200
|
if (shareMode) { tfUpdateShareBar(host, scope, level); return; }
|
|
16235
|
-
|
|
16201
|
+
appendRejectionHistory();
|
|
16236
16202
|
host.appendChild(addBtn);
|
|
16237
16203
|
wireAdd();
|
|
16238
16204
|
}
|
|
@@ -17833,6 +17799,7 @@ async function tfSwitchProjectFolder(folderPath, options) {
|
|
|
17833
17799
|
return;
|
|
17834
17800
|
}
|
|
17835
17801
|
state.projectPath = normalized.folderPath;
|
|
17802
|
+
tfInvalidateRejections();
|
|
17836
17803
|
// Cross-project leak fix (docs/rca/hub-conversation-cross-project-leak.md): drop the previous
|
|
17837
17804
|
// project's active conversation on switch. Otherwise the just-finished run stays state.activeId
|
|
17838
17805
|
// while state.projectPath is the new project, and a debounced conversation PUT during the
|