fraim-hub 2.0.286 → 2.0.287
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/server.js +67 -4
- package/dist/src/cli/setup/ide-invocation-surfaces.js +4 -1
- package/dist/src/config/persona-capability-bundles.js +10 -0
- package/dist/src/fraim/db-service.js +33 -0
- package/package.json +2 -2
- package/public/ai-hub/index.html +9 -2
- package/public/ai-hub/script.js +235 -105
- package/public/ai-hub/styles.css +71 -11
|
@@ -1916,6 +1916,24 @@ function buildManagedLoginCommand(command) {
|
|
|
1916
1916
|
function getUserHubDir() {
|
|
1917
1917
|
return path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
1918
1918
|
}
|
|
1919
|
+
// Issue #1345: recursively find first file matching name under dir.
|
|
1920
|
+
function findFileRecursive(dir, filename) {
|
|
1921
|
+
try {
|
|
1922
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
1923
|
+
const full = path_1.default.join(dir, entry.name);
|
|
1924
|
+
if (entry.isDirectory()) {
|
|
1925
|
+
const hit = findFileRecursive(full, filename);
|
|
1926
|
+
if (hit)
|
|
1927
|
+
return hit;
|
|
1928
|
+
}
|
|
1929
|
+
else if (entry.isFile() && entry.name === filename) {
|
|
1930
|
+
return full;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
catch { /* ignore unreadable dirs */ }
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1919
1937
|
function ensureDirectoryPath(projectPath) {
|
|
1920
1938
|
const trimmed = (projectPath || '').trim();
|
|
1921
1939
|
if (!trimmed) {
|
|
@@ -3242,6 +3260,7 @@ class AiHubServer {
|
|
|
3242
3260
|
// so completed conversations render "What's next?" chips after reload.
|
|
3243
3261
|
nextJobRecommendations: run.nextJobRecommendations || null,
|
|
3244
3262
|
issueNumber: run.issueNumber ?? null,
|
|
3263
|
+
executionMode: run.executionMode || null,
|
|
3245
3264
|
managedByRunId: run.managedByRunId || null,
|
|
3246
3265
|
managedByPersonaKey: run.managedByPersonaKey || null,
|
|
3247
3266
|
humanCoachingDisabled: run.humanCoachingDisabled || false,
|
|
@@ -5686,6 +5705,25 @@ class AiHubServer {
|
|
|
5686
5705
|
return res.status(404).json({ error: 'Configured agent not found.' });
|
|
5687
5706
|
return res.json((0, configured_agents_1.checkConfiguredAgentReadiness)(agent, employees));
|
|
5688
5707
|
});
|
|
5708
|
+
// Issue #1345: accept executionMode forwarded from get_fraim_job response by the local proxy.
|
|
5709
|
+
// The proxy parses the Execution Mode Context block and POSTs here; Hub script reads from run poll.
|
|
5710
|
+
this.app.post('/api/ai-hub/runs/by-session/:sessionId/execution-mode', (req, res) => {
|
|
5711
|
+
if (!this.requireTrustedHubOrigin(req, res))
|
|
5712
|
+
return;
|
|
5713
|
+
const { sessionId } = req.params;
|
|
5714
|
+
const { mode, completedRuns } = req.body;
|
|
5715
|
+
if (!sessionId)
|
|
5716
|
+
return res.status(400).json({ error: 'sessionId required' });
|
|
5717
|
+
const normalizedMode = mode === 'trusted' ? 'trusted' : 'coached';
|
|
5718
|
+
const normalizedRuns = typeof completedRuns === 'number' ? completedRuns : 0;
|
|
5719
|
+
const run = this.runRegistry.all().find((r) => r.sessionId === sessionId);
|
|
5720
|
+
if (!run)
|
|
5721
|
+
return res.status(404).json({ error: 'Run not found for session.' });
|
|
5722
|
+
this.runRegistry.update(run.id, (current) => {
|
|
5723
|
+
current.executionMode = { mode: normalizedMode, completedRuns: normalizedRuns };
|
|
5724
|
+
});
|
|
5725
|
+
return res.json({ ok: true });
|
|
5726
|
+
});
|
|
5689
5727
|
this.app.post('/api/ai-hub/runs', (req, res) => {
|
|
5690
5728
|
try {
|
|
5691
5729
|
// Issue #892: project-independent (manager/company) runs resolve a working dir
|
|
@@ -5726,6 +5764,26 @@ class AiHubServer {
|
|
|
5726
5764
|
if (!jobId) {
|
|
5727
5765
|
throw new Error('Choose a FRAIM job before starting a run.');
|
|
5728
5766
|
}
|
|
5767
|
+
// #1394: Dedup guard — return an already-running identical run rather than spawning a second
|
|
5768
|
+
// process. Key includes managerDisplay (messages[0].text) so runs for different issues are
|
|
5769
|
+
// never blocked even when they share the same jobId + hostId + projectPath.
|
|
5770
|
+
// Window of 300 ms: UI race duplicates arrive within ~100 ms (React render cycle);
|
|
5771
|
+
// sequential test runs with the same params are separated by 600 ms+ (test body +
|
|
5772
|
+
// beforeEach page.goto), so the window never fires across test boundaries.
|
|
5773
|
+
const MANAGER_DEDUP_WINDOW_MS = 300;
|
|
5774
|
+
if ((req.body.sourceTrigger ?? 'manager') === 'manager') {
|
|
5775
|
+
const now = Date.now();
|
|
5776
|
+
const activeRun = this.runRegistry.all().find((r) => r.status === 'running'
|
|
5777
|
+
&& r.projectPath === projectPath
|
|
5778
|
+
&& r.jobId === jobId
|
|
5779
|
+
&& r.hostId === hostId
|
|
5780
|
+
&& r.messages[0]?.text === managerDisplay
|
|
5781
|
+
&& now - Date.parse(r.createdAt) < MANAGER_DEDUP_WINDOW_MS);
|
|
5782
|
+
if (activeRun) {
|
|
5783
|
+
console.warn('[ai-hub] hub.duplicate_run_blocked', { projectPath, jobId, hostId, existingRunId: activeRun.id });
|
|
5784
|
+
return res.json(this.enrichRunForResponse(activeRun));
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5729
5787
|
const startTimestamp = new Date().toISOString();
|
|
5730
5788
|
const jobMetadata = this.resolveHubJob(projectPath, jobId);
|
|
5731
5789
|
const fallbackJobTitle = typeof req.body.jobTitle === 'string' && req.body.jobTitle.trim()
|
|
@@ -5765,7 +5823,9 @@ class AiHubServer {
|
|
|
5765
5823
|
phaseVisits: [],
|
|
5766
5824
|
totals: emptyTotals(),
|
|
5767
5825
|
lastStatusChangeAt: startTimestamp,
|
|
5768
|
-
|
|
5826
|
+
// Issue #1357: fall back to custom persona resolution before the catalog lookup
|
|
5827
|
+
// so custom employee owners (e.g. SidCoder for feature-implementation) are used.
|
|
5828
|
+
personaKey: jobMetadata?.personaKey ?? getCustomPersonaForJob(projectPath, jobId) ?? getHubPersonaForJob(jobId),
|
|
5769
5829
|
// Issue #892: persist the invocation scope so the run is routed to the right
|
|
5770
5830
|
// conversation bucket (manager/company get a project-independent home) and so
|
|
5771
5831
|
// the resolved fallback working dir is never mistaken for the active project.
|
|
@@ -6230,7 +6290,8 @@ class AiHubServer {
|
|
|
6230
6290
|
totals: persistedRun?.totals || emptyTotals(),
|
|
6231
6291
|
lastStatusChangeAt: now,
|
|
6232
6292
|
runDiscriminant: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
|
|
6233
|
-
|
|
6293
|
+
// Issue #1357: consult custom persona owner before falling back to catalog.
|
|
6294
|
+
personaKey: getCustomPersonaForJob(projectPath, jobId) ?? getHubPersonaForJob(jobId),
|
|
6234
6295
|
continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
|
|
6235
6296
|
};
|
|
6236
6297
|
host_session_state_1.hostSessionState.applySession(run, { configuredAgentId: configuredAgent.id, baseHostId: hostId }, sessionId, { sourceRunId: run.id, status: resolvedHostSession?.status || 'suspect' });
|
|
@@ -7025,7 +7086,8 @@ class AiHubServer {
|
|
|
7025
7086
|
phaseVisits: [],
|
|
7026
7087
|
totals: emptyTotals(),
|
|
7027
7088
|
lastStatusChangeAt: startTimestamp,
|
|
7028
|
-
|
|
7089
|
+
// Issue #1357: consult custom persona owner before falling back to catalog.
|
|
7090
|
+
personaKey: getCustomPersonaForJob(projectPath, jobName) ?? getHubPersonaForJob(jobName),
|
|
7029
7091
|
};
|
|
7030
7092
|
// Register the run before spawning so onEvent/onExit callbacks can
|
|
7031
7093
|
// safely call update() even if they fire synchronously (FakeHostRuntime).
|
|
@@ -7178,7 +7240,8 @@ class AiHubServer {
|
|
|
7178
7240
|
phaseVisits: [],
|
|
7179
7241
|
totals: emptyTotals(),
|
|
7180
7242
|
lastStatusChangeAt: startTimestamp,
|
|
7181
|
-
|
|
7243
|
+
// Issue #1357: fall back to custom persona resolution before the catalog lookup.
|
|
7244
|
+
personaKey: jobMetadata?.personaKey ?? getCustomPersonaForJob(deployment.projectPath, jobId) ?? getHubPersonaForJob(jobId),
|
|
7182
7245
|
};
|
|
7183
7246
|
// Pre-register before startRun so synchronous onEvent calls (e.g. FakeHostRuntime)
|
|
7184
7247
|
// can call runRegistry.update without "Run not found" throws.
|
|
@@ -29,7 +29,10 @@ exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE = [
|
|
|
29
29
|
'If required FRAIM MCP tools remain unavailable after the deferred-tool preload/retry step, stop the FRAIM job.',
|
|
30
30
|
'Do not continue from memory, local stubs, cached instructions, or prior context.',
|
|
31
31
|
'In the blocker sentence, state only the work-focused blocker and what outcome is blocked; do not narrate ToolSearch/tool_search, individual tool names, server internals, or retry mechanics.',
|
|
32
|
-
'Always help troubleshoot the unavailable FRAIM MCP connection: tell the user to reconnect or enable the FRAIM MCP server in their IDE/agent, restart or refresh the agent session, retry the FRAIM job, and if it still fails run `fraim doctor --test-mcp` and share the failing check output.'
|
|
32
|
+
'Always help troubleshoot the unavailable FRAIM MCP connection: tell the user to reconnect or enable the FRAIM MCP server in their IDE/agent, restart or refresh the agent session, retry the FRAIM job, and if it still fails run `fraim doctor --test-mcp` and share the failing check output.',
|
|
33
|
+
'Exception — troubleshooting FRAIM itself: if the user\'s request is to diagnose or fix a FRAIM issue and FRAIM MCP is unavailable, do NOT stop.',
|
|
34
|
+
'Instead, read `~/.fraim/docs/TROUBLESHOOTING.md` directly from disk (it is always synced in full by `fraim sync` and is available without MCP) and follow its guidance to diagnose the issue.',
|
|
35
|
+
'After completing the local diagnosis, prompt the user to re-run `fraim sync` and restart their agent session, then offer to run the `troubleshoot-fraim` FRAIM job for a structured investigation once MCP is restored.'
|
|
33
36
|
].join(' ');
|
|
34
37
|
function buildDeferredToolBootstrapSection(profile) {
|
|
35
38
|
if (profile === 'none') {
|
|
@@ -291,10 +291,20 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
|
|
|
291
291
|
// named specialist persona. They resolve through ownership (never short-circuited
|
|
292
292
|
// as "free") so the Hub attributes them to FRAIMworker, but they are never
|
|
293
293
|
// hire-gated because FRAIMworker is not a purchasable persona.
|
|
294
|
+
//
|
|
295
|
+
// Issue #1398: manager-agreements / organization-onboarding / organizational-learning-
|
|
296
|
+
// synthesis are Manager/Company area-level jobs, not tied to a named specialist — they
|
|
297
|
+
// were falling through to DEFAULT_UNASSIGNED_PERSONA_KEY ('mandy'), which misattributed
|
|
298
|
+
// them to an employee who never runs them and left them outside the Manager/Company
|
|
299
|
+
// employee rail's FRAIMworker group.
|
|
294
300
|
const GENERIC_WORKER_OWNED_JOBS = new Set([
|
|
295
301
|
'contribute-to-fraim',
|
|
296
302
|
'file-fraim-issue',
|
|
297
303
|
'praise-fraim',
|
|
304
|
+
'troubleshoot-fraim',
|
|
305
|
+
'manager-agreements',
|
|
306
|
+
'organization-onboarding',
|
|
307
|
+
'organizational-learning-synthesis',
|
|
298
308
|
]);
|
|
299
309
|
function getPersonaCapabilityBundle(personaKey) {
|
|
300
310
|
return exports.PERSONA_CAPABILITY_BUNDLES[personaKey];
|
|
@@ -170,6 +170,8 @@ class FraimDbService {
|
|
|
170
170
|
// Issue #563 — shared organization context (FRAIM-cloud backend).
|
|
171
171
|
this.orgArtifactsCollection = this.db.collection('fraim_org_artifacts');
|
|
172
172
|
this.orgAuditCollection = this.db.collection('fraim_org_audit');
|
|
173
|
+
// Issue #1345 — job execution modes (Coached/Trusted).
|
|
174
|
+
this.jobExecutionModesCollection = this.db.collection('fraim_job_execution_modes');
|
|
173
175
|
}
|
|
174
176
|
async initializeIndexes() {
|
|
175
177
|
if (!this.db)
|
|
@@ -199,6 +201,7 @@ class FraimDbService {
|
|
|
199
201
|
// requirement — swallow failures like the other Cosmos-sensitive indexes above.
|
|
200
202
|
await this.orgArtifactsCollection.createIndex({ orgId: 1, relativePath: 1 }, { unique: true }).catch(() => { });
|
|
201
203
|
await this.orgAuditCollection.createIndex({ orgId: 1, at: -1 }).catch(() => { });
|
|
204
|
+
await this.jobExecutionModesCollection.createIndex({ userId: 1, jobName: 1 }, { unique: true }).catch(() => { });
|
|
202
205
|
await this.pendingVerificationsCollection.createIndex({ email: 1 });
|
|
203
206
|
// Compound index covers `findOne({email}, { sort: { createdAt: -1 } })` —
|
|
204
207
|
// the request-access flow's lookup of the most-recent pending row per
|
|
@@ -286,6 +289,36 @@ class FraimDbService {
|
|
|
286
289
|
throw new Error('DB not connected');
|
|
287
290
|
return await this.orgAuditCollection.find({ orgId }).sort({ at: -1 }).toArray();
|
|
288
291
|
}
|
|
292
|
+
async getJobExecutionMode(userId, jobName) {
|
|
293
|
+
if (!this.jobExecutionModesCollection)
|
|
294
|
+
return null;
|
|
295
|
+
return await this.jobExecutionModesCollection.findOne({ userId, jobName }) ?? null;
|
|
296
|
+
}
|
|
297
|
+
async upsertJobExecutionMode(userId, update) {
|
|
298
|
+
if (!this.jobExecutionModesCollection)
|
|
299
|
+
return;
|
|
300
|
+
const jobName = String(update['jobName'] ?? '');
|
|
301
|
+
if (!jobName)
|
|
302
|
+
return;
|
|
303
|
+
const existing = await this.jobExecutionModesCollection.findOne({ userId, jobName });
|
|
304
|
+
let completedRuns = typeof existing?.completedRuns === 'number' ? existing.completedRuns : 0;
|
|
305
|
+
if (update['incrementRun'] === true)
|
|
306
|
+
completedRuns += 1;
|
|
307
|
+
const record = {
|
|
308
|
+
userId,
|
|
309
|
+
jobName,
|
|
310
|
+
mode: (update['mode'] === 'trusted' || update['mode'] === 'coached')
|
|
311
|
+
? update['mode']
|
|
312
|
+
: (existing?.mode ?? 'coached'),
|
|
313
|
+
completedRuns,
|
|
314
|
+
trustedSince: typeof update['trustedSince'] === 'string' ? update['trustedSince'] : existing?.trustedSince,
|
|
315
|
+
trustedAtRun: typeof update['trustedAtRun'] === 'number' ? update['trustedAtRun'] : existing?.trustedAtRun,
|
|
316
|
+
lastGraduationOfferRun: typeof update['lastGraduationOfferRun'] === 'number' ? update['lastGraduationOfferRun'] : existing?.lastGraduationOfferRun,
|
|
317
|
+
graduationSuppressedUntilRun: typeof update['graduationSuppressedUntilRun'] === 'number' ? update['graduationSuppressedUntilRun'] : existing?.graduationSuppressedUntilRun,
|
|
318
|
+
updatedAt: new Date(),
|
|
319
|
+
};
|
|
320
|
+
await this.jobExecutionModesCollection.replaceOne({ userId, jobName }, record, { upsert: true });
|
|
321
|
+
}
|
|
289
322
|
async verifyApiKey(key) {
|
|
290
323
|
if (!this.keysCollection)
|
|
291
324
|
throw new Error('DB not connected');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.287",
|
|
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",
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
"electron-updater": "^6.8.9",
|
|
211
211
|
"express": "^5.2.1",
|
|
212
212
|
"extract-zip": "^2.0.1",
|
|
213
|
-
"fraim": "2.0.
|
|
213
|
+
"fraim": "2.0.287",
|
|
214
214
|
"mongodb": "^7.0.0",
|
|
215
215
|
"node-cron": "4.2.1",
|
|
216
216
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -827,8 +827,10 @@
|
|
|
827
827
|
</div>
|
|
828
828
|
|
|
829
829
|
<!-- #1351: unified Add Agent dialog — add a configured profile directly, or delegate
|
|
830
|
-
setup to FRAIM.
|
|
831
|
-
|
|
830
|
+
setup to FRAIM. This is the single entry point for agent setup: the toolbar's
|
|
831
|
+
"Add AI agent" button and, on #1390, the per-tool quick-picks inside the Delegate
|
|
832
|
+
tab (which replaced the old standalone "setup another tool" disclosure) both open
|
|
833
|
+
this same dialog. -->
|
|
832
834
|
<div id="add-agent-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="aam-title" hidden>
|
|
833
835
|
<div class="modal-card">
|
|
834
836
|
<div class="modal-hdr">
|
|
@@ -845,6 +847,11 @@
|
|
|
845
847
|
<div class="modal-body">
|
|
846
848
|
<div id="aam-manual-pane"></div>
|
|
847
849
|
<div id="aam-delegate-pane" hidden>
|
|
850
|
+
<div id="aam-delegate-quickpicks" class="aam-delegate-quickpicks" data-testid="aam-delegate-quickpicks" hidden>
|
|
851
|
+
<p class="aam-delegate-quickpicks-label">Not set up yet on this machine:</p>
|
|
852
|
+
<div id="aam-delegate-quickpicks-list" class="aam-delegate-quickpicks-list"></div>
|
|
853
|
+
<p id="aam-delegate-quickpick-caption" class="aam-delegate-quickpick-caption" data-testid="aam-delegate-quickpick-caption" hidden></p>
|
|
854
|
+
</div>
|
|
848
855
|
<div class="np-field">
|
|
849
856
|
<label for="aam-delegate-context">Any specific direction for this run? <span class="np-optional">(optional, leave blank to start with defaults)</span></label>
|
|
850
857
|
<textarea id="aam-delegate-context" rows="4" data-testid="add-agent-delegate-context" placeholder="e.g. Configure a CLI, a cloud-credit route, or a custom profile."></textarea>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -3605,6 +3605,7 @@ function renderActive() {
|
|
|
3605
3605
|
els['active-conv'].classList.toggle('has-manager-oversight', isManagerOversightConversation(conv));
|
|
3606
3606
|
renderConversationIdentity(conv);
|
|
3607
3607
|
renderRunStatePill(conv);
|
|
3608
|
+
renderJobModeChip(conv);
|
|
3608
3609
|
syncCoachEmployeeLabel(conv);
|
|
3609
3610
|
syncThreadPanelKicker(conv);
|
|
3610
3611
|
syncCoachPanelKicker(conv);
|
|
@@ -3621,6 +3622,7 @@ function renderActive() {
|
|
|
3621
3622
|
els['micro-log'].textContent = '';
|
|
3622
3623
|
if (els['resume-command']) els['resume-command'].hidden = true;
|
|
3623
3624
|
renderedConvId = conv.id;
|
|
3625
|
+
_modeChipLastKey = null;
|
|
3624
3626
|
renderedMessageCount = 0;
|
|
3625
3627
|
renderedMessageFingerprints = [];
|
|
3626
3628
|
renderedEventCount = 0;
|
|
@@ -3866,6 +3868,65 @@ function renderConversationIdentity(conv) {
|
|
|
3866
3868
|
host.appendChild(text);
|
|
3867
3869
|
}
|
|
3868
3870
|
|
|
3871
|
+
// Issue #1345: "In Training" chip in .conv-topline for Coached-mode jobs (R10).
|
|
3872
|
+
// Shown when mode is coached (not trusted). Training instructions are now injected by the MCP.
|
|
3873
|
+
let _modeChipLastKey = null;
|
|
3874
|
+
function renderJobModeChip(conv) {
|
|
3875
|
+
const topline = document.querySelector('#active-conv > .conv-topline');
|
|
3876
|
+
const titleEl = els['active-title'];
|
|
3877
|
+
if (!topline || !titleEl) return;
|
|
3878
|
+
|
|
3879
|
+
const removeChip = () => {
|
|
3880
|
+
const old = topline.querySelector('.job-mode-chip');
|
|
3881
|
+
if (old) old.remove();
|
|
3882
|
+
_modeChipLastKey = null;
|
|
3883
|
+
};
|
|
3884
|
+
|
|
3885
|
+
const jobName = conv && conv.jobId && conv.jobId !== '__freeform__' ? conv.jobId : null;
|
|
3886
|
+
const personaKey = conv && conv.personaKey ? conv.personaKey : null;
|
|
3887
|
+
if (!jobName || !personaKey) { removeChip(); return; }
|
|
3888
|
+
|
|
3889
|
+
// Issue #1345: read executionMode from run data folded in by foldRunIntoConversation.
|
|
3890
|
+
// No fetch needed — the local proxy forwards it from the get_fraim_job response.
|
|
3891
|
+
const data = conv.executionMode;
|
|
3892
|
+
if (!data || data.mode === 'trusted') { removeChip(); return; }
|
|
3893
|
+
|
|
3894
|
+
const key = `${personaKey}:${jobName}`;
|
|
3895
|
+
if (_modeChipLastKey === key && topline.querySelector('.job-mode-chip')) return;
|
|
3896
|
+
|
|
3897
|
+
_modeChipLastKey = key;
|
|
3898
|
+
let chip = topline.querySelector('.job-mode-chip');
|
|
3899
|
+
if (!chip) {
|
|
3900
|
+
chip = document.createElement('span');
|
|
3901
|
+
chip.className = 'job-mode-chip job-mode-chip--in-training';
|
|
3902
|
+
chip.setAttribute('aria-label', 'In Training: training pauses active on this job');
|
|
3903
|
+
|
|
3904
|
+
const label = document.createElement('span');
|
|
3905
|
+
label.textContent = 'In Training';
|
|
3906
|
+
chip.appendChild(label);
|
|
3907
|
+
|
|
3908
|
+
const tooltip = document.createElement('div');
|
|
3909
|
+
tooltip.className = 'chip-tooltip';
|
|
3910
|
+
|
|
3911
|
+
const runCount = typeof data.completedRuns === 'number' ? data.completedRuns : 0;
|
|
3912
|
+
const personaDisplay = (() => {
|
|
3913
|
+
const p = (state.bootstrap && state.bootstrap.personas || []).find((p) => p.key === personaKey);
|
|
3914
|
+
return p ? p.displayName : personaKey;
|
|
3915
|
+
})();
|
|
3916
|
+
const personaDisplayEsc = tfEscape(personaDisplay);
|
|
3917
|
+
const threshold = 3;
|
|
3918
|
+
|
|
3919
|
+
tooltip.innerHTML = [
|
|
3920
|
+
`<div class="chip-tooltip-section"><div class="chip-tooltip-label">Why</div>${personaDisplayEsc} is still learning how you like this job to be structured. ${runCount === 0 ? "You haven't run it together yet." : `${runCount === 1 ? "You've done it once together" : `You've done it ${runCount} times together`} so far.`}</div>`,
|
|
3921
|
+
`<div class="chip-tooltip-section"><div class="chip-tooltip-label">What this means</div>${personaDisplayEsc} will show you an outline of what they plan to build before drafting, so you can redirect the shape before any prose is written.</div>`,
|
|
3922
|
+
`<div class="chip-tooltip-section"><div class="chip-tooltip-label">How to graduate</div>After ${threshold} completed runs, the retrospective recommends the <strong>graduate-job</strong>. Run it to promote this job to Trusted mode and skip the training pauses on future runs.</div>`,
|
|
3923
|
+
].join('');
|
|
3924
|
+
chip.appendChild(tooltip);
|
|
3925
|
+
|
|
3926
|
+
titleEl.insertAdjacentElement('afterend', chip);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3869
3930
|
function renderRunStatePill(conv) {
|
|
3870
3931
|
const pill = els['run-state-pill'];
|
|
3871
3932
|
if (!pill) return;
|
|
@@ -8282,6 +8343,8 @@ function foldRunIntoConversation(conv, run) {
|
|
|
8282
8343
|
}
|
|
8283
8344
|
// Issue #848: fold next-job recommendations + the issue pointer from the run.
|
|
8284
8345
|
conv.nextJobRecommendations = normalizeNextJobRecommendations(run.nextJobRecommendations);
|
|
8346
|
+
// Issue #1345: execution mode forwarded from get_fraim_job response via local proxy.
|
|
8347
|
+
conv.executionMode = run.executionMode || null;
|
|
8285
8348
|
if (run.issueNumber !== undefined && run.issueNumber !== null) conv.issueNumber = run.issueNumber;
|
|
8286
8349
|
if (Array.isArray(run.agentSwitches)) conv.agentSwitches = run.agentSwitches;
|
|
8287
8350
|
if (run.handoffSummary !== undefined) conv.handoffSummary = run.handoffSummary;
|
|
@@ -8448,8 +8511,10 @@ function switchToConversation(id) {
|
|
|
8448
8511
|
const info = document.getElementById(tf.area + '-info-view');
|
|
8449
8512
|
if (host) host.hidden = false;
|
|
8450
8513
|
if (info) info.hidden = true;
|
|
8451
|
-
// #
|
|
8452
|
-
|
|
8514
|
+
// #1397: an area selection represents every explicit destination, not only
|
|
8515
|
+
// the Info view. Poll-driven renders must preserve this conversation until
|
|
8516
|
+
// the user chooses another destination or the conversation disappears.
|
|
8517
|
+
if (tf.areaSelection) tf.areaSelection[tf.area] = id;
|
|
8453
8518
|
}
|
|
8454
8519
|
renderRail();
|
|
8455
8520
|
renderActive();
|
|
@@ -8807,39 +8872,6 @@ function renderConfiguredAgentsPanel() {
|
|
|
8807
8872
|
}
|
|
8808
8873
|
}
|
|
8809
8874
|
|
|
8810
|
-
const uninstalled = hubEmployees().filter((e) => !e.available);
|
|
8811
|
-
if (uninstalled.length) {
|
|
8812
|
-
const disclosure = document.createElement('details');
|
|
8813
|
-
disclosure.className = 'configured-agents-setup-disclosure';
|
|
8814
|
-
disclosure.dataset.testid = 'setup-another-tool';
|
|
8815
|
-
const summary = document.createElement('summary');
|
|
8816
|
-
summary.textContent = 'Set up another tool';
|
|
8817
|
-
disclosure.appendChild(summary);
|
|
8818
|
-
for (const emp of uninstalled) {
|
|
8819
|
-
const row = document.createElement('div');
|
|
8820
|
-
row.className = 'install-row';
|
|
8821
|
-
const label = document.createElement('span');
|
|
8822
|
-
label.className = 'install-label';
|
|
8823
|
-
label.textContent = emp.label;
|
|
8824
|
-
const empDetail = document.createElement('span');
|
|
8825
|
-
empDetail.className = 'install-status';
|
|
8826
|
-
empDetail.textContent = emp.detail || 'Not installed';
|
|
8827
|
-
const btn = document.createElement('button');
|
|
8828
|
-
btn.type = 'button';
|
|
8829
|
-
btn.className = 'small';
|
|
8830
|
-
btn.textContent = 'Set up';
|
|
8831
|
-
btn.dataset.testid = `hub-agent-install-${emp.id}`;
|
|
8832
|
-
btn.addEventListener('click', () => openAddAgentModal(
|
|
8833
|
-
'delegate',
|
|
8834
|
-
`Set up ${emp.label}: install the CLI, sign in, and configure it as a Hub agent.`
|
|
8835
|
-
));
|
|
8836
|
-
row.appendChild(label);
|
|
8837
|
-
row.appendChild(empDetail);
|
|
8838
|
-
row.appendChild(btn);
|
|
8839
|
-
disclosure.appendChild(row);
|
|
8840
|
-
}
|
|
8841
|
-
panel.appendChild(disclosure);
|
|
8842
|
-
}
|
|
8843
8875
|
}
|
|
8844
8876
|
|
|
8845
8877
|
// #1351: unified Add Agent dialog. "Add Manually" hosts the existing structured
|
|
@@ -8851,16 +8883,59 @@ function renderConfiguredAgentsPanel() {
|
|
|
8851
8883
|
// dialog's tab chrome would show an unrelated "Add Manually" tab next to those jobs.
|
|
8852
8884
|
// Reusing tfStartOnboardingJob (the actual capability) rather than the modal markup
|
|
8853
8885
|
// (incidental UI chrome) is the correct level of reuse here.
|
|
8886
|
+
//
|
|
8887
|
+
// #1390: this is the single setup entry point for AI agents. The panel's toolbar
|
|
8888
|
+
// "Add AI agent" button is the only affordance rendered inline on the AI Agents tab;
|
|
8889
|
+
// the per-tool shortcuts that used to live in a standalone "Set up another tool"
|
|
8890
|
+
// disclosure on that panel now live as quick-pick chips inside this dialog's Delegate
|
|
8891
|
+
// tab (renderAddAgentDelegateQuickpicks), so there is exactly one place to start setup.
|
|
8854
8892
|
function openAddAgentModal(tab, delegateMessage) {
|
|
8855
8893
|
state._addAgentDelegateMessage = delegateMessage
|
|
8856
8894
|
|| 'Set up a new Hub AI agent: configure a CLI, cloud-credit route, or custom profile.';
|
|
8895
|
+
state._addAgentDelegateEmployeeId = null;
|
|
8857
8896
|
const ctx = document.getElementById('aam-delegate-context');
|
|
8858
8897
|
if (ctx) ctx.value = '';
|
|
8898
|
+
renderAddAgentDelegateQuickpicks();
|
|
8859
8899
|
const modal = document.getElementById('add-agent-modal');
|
|
8860
8900
|
if (modal) modal.hidden = false;
|
|
8861
8901
|
setAddAgentTab(tab === 'delegate' ? 'delegate' : 'manual');
|
|
8862
8902
|
}
|
|
8863
8903
|
|
|
8904
|
+
// #1390: quick-pick chips for tools not yet installed on this machine, shown inside
|
|
8905
|
+
// the Delegate tab. Clicking one targets the delegate job at that specific tool —
|
|
8906
|
+
// the same outcome the old standalone "Set up another tool" disclosure gave, without
|
|
8907
|
+
// a second setup entry point on the AI Agents panel itself.
|
|
8908
|
+
function renderAddAgentDelegateQuickpicks() {
|
|
8909
|
+
const wrap = document.getElementById('aam-delegate-quickpicks');
|
|
8910
|
+
const list = document.getElementById('aam-delegate-quickpicks-list');
|
|
8911
|
+
const caption = document.getElementById('aam-delegate-quickpick-caption');
|
|
8912
|
+
if (!wrap || !list) return;
|
|
8913
|
+
list.innerHTML = '';
|
|
8914
|
+
const uninstalled = hubEmployees().filter((e) => !e.available);
|
|
8915
|
+
wrap.hidden = !uninstalled.length;
|
|
8916
|
+
for (const emp of uninstalled) {
|
|
8917
|
+
const chip = document.createElement('button');
|
|
8918
|
+
chip.type = 'button';
|
|
8919
|
+
chip.className = 'jf-chip aam-delegate-quickpick';
|
|
8920
|
+
chip.textContent = emp.label;
|
|
8921
|
+
chip.dataset.testid = `hub-agent-install-${emp.id}`;
|
|
8922
|
+
chip.classList.toggle('active', state._addAgentDelegateEmployeeId === emp.id);
|
|
8923
|
+
chip.addEventListener('click', () => selectAddAgentDelegateQuickpick(emp));
|
|
8924
|
+
list.appendChild(chip);
|
|
8925
|
+
}
|
|
8926
|
+
if (caption) {
|
|
8927
|
+
const selected = uninstalled.find((e) => e.id === state._addAgentDelegateEmployeeId);
|
|
8928
|
+
caption.textContent = selected ? state._addAgentDelegateMessage : '';
|
|
8929
|
+
caption.hidden = !selected;
|
|
8930
|
+
}
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8933
|
+
function selectAddAgentDelegateQuickpick(emp) {
|
|
8934
|
+
state._addAgentDelegateEmployeeId = emp.id;
|
|
8935
|
+
state._addAgentDelegateMessage = `Set up ${emp.label}: install the CLI, sign in, and configure it as a Hub agent.`;
|
|
8936
|
+
renderAddAgentDelegateQuickpicks();
|
|
8937
|
+
}
|
|
8938
|
+
|
|
8864
8939
|
function closeAddAgentModal() {
|
|
8865
8940
|
const modal = document.getElementById('add-agent-modal');
|
|
8866
8941
|
if (modal) modal.hidden = true;
|
|
@@ -9998,12 +10073,11 @@ const tf = {
|
|
|
9998
10073
|
// #769: hire-pending state for post-Stripe redirect polling
|
|
9999
10074
|
hirePendingState: null, // null | 'polling' | 'timeout'
|
|
10000
10075
|
hirePendingBaseCount: 0, // hired persona count at polling start
|
|
10001
|
-
// #
|
|
10002
|
-
//
|
|
10003
|
-
//
|
|
10004
|
-
//
|
|
10005
|
-
|
|
10006
|
-
viewPinned: { manager: false, company: false },
|
|
10076
|
+
// #1397: the explicit destination selected in each area. `null` means the area
|
|
10077
|
+
// may auto-home to its most relevant conversation; `info` and conversation IDs
|
|
10078
|
+
// are user choices that survive every poll-driven render. This replaces the
|
|
10079
|
+
// former info-only viewPinned special case with one navigation-state contract.
|
|
10080
|
+
areaSelection: { manager: null, company: null },
|
|
10007
10081
|
};
|
|
10008
10082
|
|
|
10009
10083
|
// ---------------------------------------------------------------------------
|
|
@@ -12950,6 +13024,52 @@ function tfActiveMgrConv() {
|
|
|
12950
13024
|
);
|
|
12951
13025
|
}
|
|
12952
13026
|
|
|
13027
|
+
// #1397: resolve one stable destination for an area. Polling may refresh the
|
|
13028
|
+
// conversations behind that destination, but it may not choose a different one.
|
|
13029
|
+
// A deleted or re-scoped conversation invalidates the selection and restores the
|
|
13030
|
+
// area's normal auto-home behavior.
|
|
13031
|
+
function tfResolveAreaView(area, fallbackConv) {
|
|
13032
|
+
const selected = tf.areaSelection && tf.areaSelection[area];
|
|
13033
|
+
if (selected === 'info') return { info: true, conversation: null };
|
|
13034
|
+
if (selected) {
|
|
13035
|
+
const conv = findConversation(selected);
|
|
13036
|
+
if (conv && convScope(conv) === area) return { info: false, conversation: conv };
|
|
13037
|
+
tf.areaSelection[area] = null;
|
|
13038
|
+
}
|
|
13039
|
+
return { info: !fallbackConv, conversation: fallbackConv || null };
|
|
13040
|
+
}
|
|
13041
|
+
|
|
13042
|
+
function tfAreaScopedConversations(area) {
|
|
13043
|
+
return Object.values(state.conversations || {}).flat()
|
|
13044
|
+
.filter((conv) => conv && convScope(conv) === area)
|
|
13045
|
+
.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
|
13046
|
+
}
|
|
13047
|
+
|
|
13048
|
+
// Every area has FRAIMworker. Named employees join an area when that area is
|
|
13049
|
+
// their home (Ashley on Manager) or when they actually have a run there. This
|
|
13050
|
+
// keeps other hired project employees out of Manager/Company until relevant,
|
|
13051
|
+
// while ensuring every scoped run remains reachable under its owner.
|
|
13052
|
+
function tfAreaRailPersonas(area) {
|
|
13053
|
+
const personas = new Map([[GENERIC_WORKER_PERSONA.key, GENERIC_WORKER_PERSONA]]);
|
|
13054
|
+
const bootstrapPersonas = (state.bootstrap && state.bootstrap.personas) || [];
|
|
13055
|
+
if (area === 'manager') {
|
|
13056
|
+
const catalogJobs = (state.bootstrap && state.bootstrap.jobs) || [];
|
|
13057
|
+
for (const persona of bootstrapPersonas) {
|
|
13058
|
+
if (!MANAGER_PERSONA_KEYS.has(persona.key) || !tfIsPersonaHired(persona.key)) continue;
|
|
13059
|
+
// Defensive: only give a home persona an employee group if the catalog actually has
|
|
13060
|
+
// a job that requires them, mirroring the original Manager-tab-only guard.
|
|
13061
|
+
if (!catalogJobs.some((j) => j && j.requiredPersonaKey === persona.key)) continue;
|
|
13062
|
+
personas.set(persona.key, persona);
|
|
13063
|
+
}
|
|
13064
|
+
}
|
|
13065
|
+
for (const conv of tfAreaScopedConversations(area)) {
|
|
13066
|
+
if (!conv.personaKey || personas.has(conv.personaKey)) continue;
|
|
13067
|
+
const persona = getConversationPersona(conv);
|
|
13068
|
+
if (persona) personas.set(persona.key, persona);
|
|
13069
|
+
}
|
|
13070
|
+
return [...personas.values()];
|
|
13071
|
+
}
|
|
13072
|
+
|
|
12953
13073
|
// #702: is a persona hired (has a company seat) so its manager-tab jobs should show?
|
|
12954
13074
|
// (MANAGER_ASHLEY_JOBS is defined once at the top of this file as the single source of truth.)
|
|
12955
13075
|
function tfIsPersonaHired(personaKey) {
|
|
@@ -12970,7 +13090,7 @@ function tfStartManagerPersonaJob(jobId) {
|
|
|
12970
13090
|
// identical on both tabs.
|
|
12971
13091
|
function tfBuildManagerRunItem(conv) {
|
|
12972
13092
|
const btn = document.createElement('button');
|
|
12973
|
-
btn.className = 'conv-item';
|
|
13093
|
+
btn.className = 'conv-item' + (state.activeId === conv.id ? ' active' : '');
|
|
12974
13094
|
btn.type = 'button';
|
|
12975
13095
|
btn.dataset.conv = conv.id;
|
|
12976
13096
|
const body = document.createElement('span'); body.className = 'conv-body';
|
|
@@ -12995,11 +13115,9 @@ function tfBuildManagerRunItem(conv) {
|
|
|
12995
13115
|
// visual as the Projects rail employee groups (avatar + name + role + count + "+" that
|
|
12996
13116
|
// opens the job palette), with the persona's MANAGER-invoked runs listed under it.
|
|
12997
13117
|
// Keeps a persona looking like an employee, consistent across tabs.
|
|
12998
|
-
function
|
|
13118
|
+
function tfRenderAreaPersonaGroup(rail, persona, area) {
|
|
12999
13119
|
const sample = { personaKey: persona.key };
|
|
13000
|
-
const runs =
|
|
13001
|
-
.filter((c) => c && c.personaKey === persona.key && convScope(c) === 'manager')
|
|
13002
|
-
.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
|
13120
|
+
const runs = tfAreaScopedConversations(area).filter((c) => c.personaKey === persona.key);
|
|
13003
13121
|
const details = document.createElement('details');
|
|
13004
13122
|
details.className = 'conv-employee-group';
|
|
13005
13123
|
details.open = true;
|
|
@@ -13018,8 +13136,8 @@ function tfRenderManagerPersonaGroup(rail, persona) {
|
|
|
13018
13136
|
addBtn.setAttribute('aria-label', 'Launch a job for ' + (persona.displayName || persona.key));
|
|
13019
13137
|
addBtn.addEventListener('click', (e) => {
|
|
13020
13138
|
e.preventDefault(); e.stopPropagation();
|
|
13021
|
-
// Launch from
|
|
13022
|
-
//
|
|
13139
|
+
// Launch from this area: palette prefiltered to the employee. startRun stamps
|
|
13140
|
+
// the active area on the conversation so it returns to the same rail.
|
|
13023
13141
|
openPalette({ employeeId: (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.employeeId) || 'claude', prefixSearch: '/' + persona.key });
|
|
13024
13142
|
});
|
|
13025
13143
|
const count = document.createElement('span'); count.className = 'conv-employee-tab-count';
|
|
@@ -13038,6 +13156,39 @@ function tfRenderManagerPersonaGroup(rail, persona) {
|
|
|
13038
13156
|
rail.appendChild(details);
|
|
13039
13157
|
}
|
|
13040
13158
|
|
|
13159
|
+
// #1397/#1398: shared employee rail for the Manager and Company tabs. FRAIMworker is
|
|
13160
|
+
// always present (issue #1398 — every area has a generic owner to fall back to); named
|
|
13161
|
+
// employees join when the area is their home (Ashley on Manager) or they actually have
|
|
13162
|
+
// a run there (e.g. Aida's manager-scoped runs), via tfAreaRailPersonas. `ownerlessConv`
|
|
13163
|
+
// is the area's own top-level job run (manager-agreements / org onboarding) or any other
|
|
13164
|
+
// run with no persona group to live under; it gets its own rail entry unless a group
|
|
13165
|
+
// below will already list it.
|
|
13166
|
+
function tfRenderAreaEmployeeRail(rail, area, ownerlessConv) {
|
|
13167
|
+
const areaPersonas = tfAreaRailPersonas(area);
|
|
13168
|
+
// A group only actually lists conversations tfAreaScopedConversations(area) returns
|
|
13169
|
+
// (i.e. correctly scope/invokedArea-tagged). A legacy conv that predates that tagging
|
|
13170
|
+
// (see #1351) can share a persona key with a rail group yet not appear inside it — it
|
|
13171
|
+
// still needs its own top-level entry, or it becomes unreachable.
|
|
13172
|
+
const scopedIds = new Set(tfAreaScopedConversations(area).map((c) => c.id));
|
|
13173
|
+
const ownedByGroup = !!(ownerlessConv && ownerlessConv.personaKey && scopedIds.has(ownerlessConv.id)
|
|
13174
|
+
&& areaPersonas.some((p) => p.key === ownerlessConv.personaKey));
|
|
13175
|
+
if (ownerlessConv && !ownedByGroup) rail.appendChild(tfBuildManagerRunItem(ownerlessConv));
|
|
13176
|
+
let any = false;
|
|
13177
|
+
for (const persona of areaPersonas) {
|
|
13178
|
+
if (!any) {
|
|
13179
|
+
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Your employees';
|
|
13180
|
+
rail.appendChild(head);
|
|
13181
|
+
any = true;
|
|
13182
|
+
}
|
|
13183
|
+
tfRenderAreaPersonaGroup(rail, persona, area);
|
|
13184
|
+
}
|
|
13185
|
+
if (any) {
|
|
13186
|
+
const pNote = document.createElement('div'); pNote.className = 'area-rail-note';
|
|
13187
|
+
pNote.textContent = "These employees work across every project you manage. Launch a job here (+) for cross-project work. Its run stays on this tab; the same employee's job launched inside a project stays there. Coach and verify like any employee.";
|
|
13188
|
+
rail.appendChild(pNote);
|
|
13189
|
+
}
|
|
13190
|
+
}
|
|
13191
|
+
|
|
13041
13192
|
// Move the shared .page conversation panel into the specified area's workspace-conv host.
|
|
13042
13193
|
// All .workspace-conv CSS rules (header/rail hidden, conv fills space) apply automatically
|
|
13043
13194
|
// because the area-conv-host elements carry the workspace-conv class.
|
|
@@ -13062,12 +13213,13 @@ function tfToggleAreaView(area, view) {
|
|
|
13062
13213
|
tfEnsurePageInArea('projects');
|
|
13063
13214
|
state.activeId = null;
|
|
13064
13215
|
renderActive();
|
|
13065
|
-
// #
|
|
13066
|
-
//
|
|
13067
|
-
//
|
|
13068
|
-
//
|
|
13069
|
-
// in this area, or by the user
|
|
13070
|
-
|
|
13216
|
+
// #1397: a manual switch to the info view is a deliberate navigation choice, exactly
|
|
13217
|
+
// like picking a specific conversation — record it as this area's explicit selection so
|
|
13218
|
+
// the next poll-driven tfRenderManager()/tfRenderCompany() doesn't silently flip back to
|
|
13219
|
+
// the run view just because a conversation is still running. Overwritten by
|
|
13220
|
+
// tfStartOnboardingJob() when a genuinely new run starts in this area, or by the user
|
|
13221
|
+
// opening a run directly (switchToConversation).
|
|
13222
|
+
if (tf.areaSelection && (area === 'manager' || area === 'company')) tf.areaSelection[area] = 'info';
|
|
13071
13223
|
}
|
|
13072
13224
|
}
|
|
13073
13225
|
|
|
@@ -13095,10 +13247,14 @@ function tfRenderCompany() {
|
|
|
13095
13247
|
// Issue #1124: when a company-scoped org conv exists, add a visible rail entry so
|
|
13096
13248
|
// the user can see and navigate to it. Previously the rail had only the static info
|
|
13097
13249
|
// button and the conversation was unreachable from the left nav.
|
|
13098
|
-
if (orgConv) rail.appendChild(tfBuildManagerRunItem(orgConv));
|
|
13099
13250
|
// #693 R1 (PR round 2): the "Company jobs" launcher list is retired. Its jobs
|
|
13100
13251
|
// now run from the section they populate — Run Organization Onboarding in
|
|
13101
13252
|
// "Context & rules", Synthesize company learnings in "Company learnings".
|
|
13253
|
+
// #1398: FRAIMworker (and any hired employee with a company-scoped run, e.g. Aida)
|
|
13254
|
+
// get the same employee-group presentation as the Manager rail — not just an orphan
|
|
13255
|
+
// entry for the single "most active" org conv.
|
|
13256
|
+
tfRenderAreaEmployeeRail(rail, 'company', orgConv);
|
|
13257
|
+
tfReopenPendingRunDelete(rail);
|
|
13102
13258
|
}
|
|
13103
13259
|
const profile = document.getElementById('company-profile');
|
|
13104
13260
|
const learn = document.getElementById('company-learnings');
|
|
@@ -13166,10 +13322,12 @@ function tfRenderCompany() {
|
|
|
13166
13322
|
const orgConv = tfActiveOrgConv();
|
|
13167
13323
|
const host = document.getElementById('company-conv-host');
|
|
13168
13324
|
const info = document.getElementById('company-info-view');
|
|
13169
|
-
// #
|
|
13170
|
-
//
|
|
13171
|
-
|
|
13172
|
-
|
|
13325
|
+
// #1397: resolve the area's one stable destination — an explicit conversation choice,
|
|
13326
|
+
// an explicit "Company Info" choice, or (absent either) the org conv auto-home. Replaces
|
|
13327
|
+
// the former Info-only tf.viewPinned special case with one navigation-state contract.
|
|
13328
|
+
const companyView = tfResolveAreaView('company', orgConv);
|
|
13329
|
+
if (tf.area === 'company' && !companyView.info && companyView.conversation) {
|
|
13330
|
+
state.activeId = companyView.conversation.id;
|
|
13173
13331
|
tfEnsurePageInArea('company');
|
|
13174
13332
|
if (host) host.hidden = false;
|
|
13175
13333
|
if (info) info.hidden = true;
|
|
@@ -13251,46 +13409,14 @@ function tfRenderManager() {
|
|
|
13251
13409
|
infoBtn.textContent = '📋 Manager Info';
|
|
13252
13410
|
infoBtn.addEventListener('click', () => tfToggleAreaView('manager', 'info'));
|
|
13253
13411
|
rail.appendChild(infoBtn);
|
|
13254
|
-
// #1351: mirror tfRenderCompany's "if (orgConv) rail.appendChild(tfBuildManagerRunItem(orgConv))"
|
|
13255
|
-
// pattern. Without this, a manager-scope run with no persona owner (create-hub-configured-agent,
|
|
13256
|
-
// manager-agreements) has NO rail entry at all — the persona-group loop below only lists runs
|
|
13257
|
-
// tagged with a hired persona's own key. That leaves the run reachable only while its poll tick
|
|
13258
|
-
// forces the run view open; the moment the user pins to "Manager Info" (or the pin survives a
|
|
13259
|
-
// tab switch), there is no way back to it. Guarded so a persona-owned active conv is not listed
|
|
13260
|
-
// twice (once here, once in its own employee group below).
|
|
13261
|
-
const mgrConvOwnedByGroup = !!(mgrConv && mgrConv.personaKey
|
|
13262
|
-
&& MANAGER_PERSONA_KEYS.has(mgrConv.personaKey) && tfIsPersonaHired(mgrConv.personaKey));
|
|
13263
|
-
if (mgrConv && !mgrConvOwnedByGroup) rail.appendChild(tfBuildManagerRunItem(mgrConv));
|
|
13264
13412
|
// #693 R1 (PR round 2): the "Manager jobs" launcher list is retired.
|
|
13265
13413
|
// Manager agreements now runs from the "Context & rules" section it populates.
|
|
13266
|
-
// Issue #702:
|
|
13267
|
-
//
|
|
13268
|
-
//
|
|
13269
|
-
//
|
|
13270
|
-
//
|
|
13271
|
-
|
|
13272
|
-
const mgrPersonas = (state.bootstrap && state.bootstrap.personas) || [];
|
|
13273
|
-
const catalogJobs = (state.bootstrap && state.bootstrap.jobs) || [];
|
|
13274
|
-
let anyMgrEmployee = false;
|
|
13275
|
-
for (const persona of mgrPersonas) {
|
|
13276
|
-
// Only manager-scoped personas (Ashley) live on the Manager tab; project employees
|
|
13277
|
-
// are shown on Projects, not here (#702 R1b). Without this, an all-hired legacy
|
|
13278
|
-
// workspace surfaced every employee on the Manager rail.
|
|
13279
|
-
if (!MANAGER_PERSONA_KEYS.has(persona.key)) continue;
|
|
13280
|
-
if (!tfIsPersonaHired(persona.key)) continue;
|
|
13281
|
-
if (!catalogJobs.some((j) => j && j.requiredPersonaKey === persona.key)) continue;
|
|
13282
|
-
if (!anyMgrEmployee) {
|
|
13283
|
-
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Your employees';
|
|
13284
|
-
rail.appendChild(head);
|
|
13285
|
-
anyMgrEmployee = true;
|
|
13286
|
-
}
|
|
13287
|
-
tfRenderManagerPersonaGroup(rail, persona);
|
|
13288
|
-
}
|
|
13289
|
-
if (anyMgrEmployee) {
|
|
13290
|
-
const pNote = document.createElement('div'); pNote.className = 'area-rail-note';
|
|
13291
|
-
pNote.textContent = "These employees work across every project you manage. Launch a job here (+) for cross-project work. Its run stays on this tab; the same employee's job launched inside a project stays there. Coach and verify like any employee.";
|
|
13292
|
-
rail.appendChild(pNote);
|
|
13293
|
-
}
|
|
13414
|
+
// Issue #702/#1398: render an EMPLOYEE GROUP for FRAIMworker plus every hired persona
|
|
13415
|
+
// that is either home to this area (Ashley on Manager) or has an actual run here (e.g.
|
|
13416
|
+
// Aida's manager-scoped runs) — the same avatar + name + runs + "+" presentation as the
|
|
13417
|
+
// Projects rail. `mgrConv` (the area's own top-level run, e.g. manager-agreements) gets
|
|
13418
|
+
// its own rail entry unless a group above already lists it.
|
|
13419
|
+
tfRenderAreaEmployeeRail(rail, 'manager', mgrConv);
|
|
13294
13420
|
// #824: the manager rail also attaches run-delete controls; re-attach an open
|
|
13295
13421
|
// confirm this innerHTML='' rebuild would otherwise drop.
|
|
13296
13422
|
tfReopenPendingRunDelete(rail);
|
|
@@ -13391,10 +13517,11 @@ function tfRenderManager() {
|
|
|
13391
13517
|
const mgrConv = tfActiveMgrConv();
|
|
13392
13518
|
const mgrHost = document.getElementById('manager-conv-host');
|
|
13393
13519
|
const mgrInfo = document.getElementById('manager-info-view');
|
|
13394
|
-
// #
|
|
13395
|
-
//
|
|
13396
|
-
|
|
13397
|
-
|
|
13520
|
+
// #1397: resolve the area's one stable destination — see tfRenderCompany's companyView
|
|
13521
|
+
// for the full rationale. Replaces the former Info-only tf.viewPinned special case.
|
|
13522
|
+
const managerView = tfResolveAreaView('manager', mgrConv);
|
|
13523
|
+
if (tf.area === 'manager' && !managerView.info && managerView.conversation) {
|
|
13524
|
+
state.activeId = managerView.conversation.id;
|
|
13398
13525
|
tfEnsurePageInArea('manager');
|
|
13399
13526
|
if (mgrHost) mgrHost.hidden = false;
|
|
13400
13527
|
if (mgrInfo) mgrInfo.hidden = true;
|
|
@@ -14938,11 +15065,14 @@ async function tfStartOnboardingJob(jobId, message, targetArea, userContext) {
|
|
|
14938
15065
|
const finalMessage = (userContext && userContext.trim()) ? userContext.trim() : jobMessage;
|
|
14939
15066
|
if (job && typeof startRun === 'function') {
|
|
14940
15067
|
if (targetArea) tfShowArea(targetArea);
|
|
14941
|
-
// #1351: a genuinely new run in this area always wins over an earlier pin to
|
|
14942
|
-
// the info view — otherwise the just-started run (e.g. create-hub-configured-agent)
|
|
14943
|
-
// would stay invisible behind a stale manual "Manager Info" choice.
|
|
14944
|
-
if (tf.viewPinned && (targetArea === 'manager' || targetArea === 'company')) tf.viewPinned[targetArea] = false;
|
|
14945
15068
|
await startRun(job, finalMessage, employeeId, undefined, targetArea);
|
|
15069
|
+
// #1397: a genuinely new run in this area is itself an explicit destination — it must
|
|
15070
|
+
// win over an earlier selection (Info view or a different conversation), otherwise the
|
|
15071
|
+
// just-started run (e.g. create-hub-configured-agent) would stay invisible behind a
|
|
15072
|
+
// stale manual choice. startRun() already set state.activeId to the new conversation.
|
|
15073
|
+
if (tf.areaSelection && (targetArea === 'manager' || targetArea === 'company') && state.activeId) {
|
|
15074
|
+
tf.areaSelection[targetArea] = state.activeId;
|
|
15075
|
+
}
|
|
14946
15076
|
// After run creation refresh the area panel so the conversation becomes visible.
|
|
14947
15077
|
if (targetArea === 'company') tfRenderCompany();
|
|
14948
15078
|
else if (targetArea === 'manager') tfRenderManager();
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -1033,6 +1033,66 @@ img.conv-employee-avatar {
|
|
|
1033
1033
|
color: #2e7d32;
|
|
1034
1034
|
font-weight: 800;
|
|
1035
1035
|
}
|
|
1036
|
+
/* Issue #1345: job execution mode chip — shown in .conv-topline for Coached-mode jobs. */
|
|
1037
|
+
.job-mode-chip {
|
|
1038
|
+
font-size: 10px;
|
|
1039
|
+
font-weight: 600;
|
|
1040
|
+
letter-spacing: .03em;
|
|
1041
|
+
padding: 2px 8px;
|
|
1042
|
+
border-radius: 999px;
|
|
1043
|
+
flex-shrink: 0;
|
|
1044
|
+
cursor: default;
|
|
1045
|
+
position: relative;
|
|
1046
|
+
}
|
|
1047
|
+
.job-mode-chip--in-training {
|
|
1048
|
+
background: rgba(0, 88, 176, 0.10);
|
|
1049
|
+
color: #0058b0;
|
|
1050
|
+
border: 1px solid rgba(0, 88, 176, 0.18);
|
|
1051
|
+
}
|
|
1052
|
+
[data-theme="dark"] .job-mode-chip--in-training {
|
|
1053
|
+
background: rgba(96, 160, 224, 0.14);
|
|
1054
|
+
color: #60a0e0;
|
|
1055
|
+
border-color: rgba(96, 160, 224, 0.22);
|
|
1056
|
+
}
|
|
1057
|
+
.job-mode-chip--in-training .chip-tooltip {
|
|
1058
|
+
display: none;
|
|
1059
|
+
position: absolute;
|
|
1060
|
+
top: calc(100% + 8px);
|
|
1061
|
+
left: 0;
|
|
1062
|
+
width: 260px;
|
|
1063
|
+
background: #fff;
|
|
1064
|
+
border: 1px solid #d0d0d5;
|
|
1065
|
+
border-radius: 10px;
|
|
1066
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.12);
|
|
1067
|
+
padding: 12px 14px;
|
|
1068
|
+
font-size: 12px;
|
|
1069
|
+
color: #1d1d1f;
|
|
1070
|
+
line-height: 1.6;
|
|
1071
|
+
z-index: var(--z-dropdown-menu);
|
|
1072
|
+
pointer-events: none;
|
|
1073
|
+
white-space: normal;
|
|
1074
|
+
text-transform: none;
|
|
1075
|
+
letter-spacing: normal;
|
|
1076
|
+
font-weight: 400;
|
|
1077
|
+
}
|
|
1078
|
+
[data-theme="dark"] .job-mode-chip--in-training .chip-tooltip {
|
|
1079
|
+
background: #2a2a2e;
|
|
1080
|
+
border-color: #3a3a3f;
|
|
1081
|
+
color: #e8e8ea;
|
|
1082
|
+
}
|
|
1083
|
+
.job-mode-chip--in-training:hover .chip-tooltip { display: block; }
|
|
1084
|
+
.chip-tooltip-section { margin-bottom: 8px; }
|
|
1085
|
+
.chip-tooltip-section:last-child { margin-bottom: 0; }
|
|
1086
|
+
.chip-tooltip-label {
|
|
1087
|
+
font-size: 10px;
|
|
1088
|
+
font-weight: 700;
|
|
1089
|
+
letter-spacing: .04em;
|
|
1090
|
+
text-transform: uppercase;
|
|
1091
|
+
color: #0058b0;
|
|
1092
|
+
margin-bottom: 2px;
|
|
1093
|
+
}
|
|
1094
|
+
[data-theme="dark"] .chip-tooltip-label { color: #60a0e0; }
|
|
1095
|
+
|
|
1036
1096
|
.conv-header {
|
|
1037
1097
|
display: flex;
|
|
1038
1098
|
flex-wrap: wrap;
|
|
@@ -2361,21 +2421,21 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
|
|
|
2361
2421
|
.install-label { font-weight: 500; min-width: 90px; }
|
|
2362
2422
|
.install-status { color: var(--muted); flex: 1; font-size: 12px; }
|
|
2363
2423
|
button.small { padding: 4px 10px; font-size: 12px; }
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
}
|
|
2369
|
-
.configured-agents-setup-disclosure > summary {
|
|
2424
|
+
/* #1390: quick-pick chips for uninstalled tools inside the Add Agent dialog's
|
|
2425
|
+
Delegate tab — the single setup entry point (see aam-* rules above). */
|
|
2426
|
+
.aam-delegate-quickpicks { margin-bottom: 14px; }
|
|
2427
|
+
.aam-delegate-quickpicks-label {
|
|
2370
2428
|
font-size: 12px;
|
|
2371
2429
|
font-weight: 600;
|
|
2372
2430
|
color: var(--muted);
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2431
|
+
margin: 0 0 6px;
|
|
2432
|
+
}
|
|
2433
|
+
.aam-delegate-quickpicks-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
2434
|
+
.aam-delegate-quickpick-caption {
|
|
2435
|
+
font-size: 12px;
|
|
2436
|
+
color: var(--muted);
|
|
2437
|
+
margin: 8px 0 0;
|
|
2376
2438
|
}
|
|
2377
|
-
.configured-agents-setup-disclosure > summary::before { content: '▸ '; }
|
|
2378
|
-
.configured-agents-setup-disclosure[open] > summary::before { content: '▾ '; }
|
|
2379
2439
|
|
|
2380
2440
|
@media (max-width: 820px) {
|
|
2381
2441
|
/* Single-column reflow — the rigid 100vh layout doesn't make sense at
|