fraim-framework 2.0.185 → 2.0.187
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/conversation-store.js +97 -6
- package/dist/src/ai-hub/hosts.js +127 -15
- package/dist/src/ai-hub/openclaw-bridge.js +17 -6
- package/dist/src/ai-hub/server.js +215 -38
- package/dist/src/cli/commands/add-ide.js +3 -2
- package/dist/src/cli/commands/add-provider.js +7 -1
- package/dist/src/cli/commands/sync.js +2 -1
- package/dist/src/cli/mcp/ide-formats.js +36 -1
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/setup/ide-detector.js +29 -1
- package/dist/src/cli/setup/ide-global-integration.js +1 -1
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +8 -2
- package/dist/src/first-run/types.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/script.js +484 -28
- package/public/ai-hub/styles.css +30 -0
package/public/ai-hub/script.js
CHANGED
|
@@ -16,7 +16,25 @@ const TREE_WIDTH_MAX = 380;
|
|
|
16
16
|
const TREE_WIDTH_DEFAULT = 216;
|
|
17
17
|
const PAGE_SCOPED_JOBS = new Set(['organization-onboarding', 'manager-agreements', 'project-onboarding', 'organizational-learning-synthesis']);
|
|
18
18
|
// Jobs scoped to the Company/Manager area — never shown in the Projects workspace rail.
|
|
19
|
+
// These are the truly area-only jobs. Issue #702: persona jobs (e.g. Ashley's) are NOT
|
|
20
|
+
// listed here — a persona can run a job from a project OR from the Manager tab, and a
|
|
21
|
+
// run's placement is decided per-invocation by conv.invokedArea, not by job id.
|
|
19
22
|
const AREA_SCOPED_JOBS = new Set(['organization-onboarding', 'organizational-learning-synthesis', 'manager-agreements']);
|
|
23
|
+
// Issue #708: project-independent conversation scope buckets (match server sentinels).
|
|
24
|
+
const MANAGER_CONV_KEY = '@manager';
|
|
25
|
+
const COMPANY_CONV_KEY = '@company';
|
|
26
|
+
// A conversation's scope: prefer the persisted `scope`, fall back to the legacy `invokedArea`.
|
|
27
|
+
function convScope(conv) {
|
|
28
|
+
const s = conv && (conv.scope || conv.invokedArea);
|
|
29
|
+
return (s === 'manager' || s === 'company') ? s : 'project';
|
|
30
|
+
}
|
|
31
|
+
// Which state.conversations bucket a conversation belongs to.
|
|
32
|
+
function convBucketKey(conv) {
|
|
33
|
+
const s = convScope(conv);
|
|
34
|
+
if (s === 'manager') return MANAGER_CONV_KEY;
|
|
35
|
+
if (s === 'company') return COMPANY_CONV_KEY;
|
|
36
|
+
return (conv && typeof conv.projectPath === 'string' && conv.projectPath) ? conv.projectPath : state.projectPath;
|
|
37
|
+
}
|
|
20
38
|
|
|
21
39
|
const state = {
|
|
22
40
|
bootstrap: null,
|
|
@@ -117,7 +135,11 @@ async function requestJson(url, options) {
|
|
|
117
135
|
if (!response.ok) {
|
|
118
136
|
const errVal = payload && payload.error;
|
|
119
137
|
const message = (errVal && typeof errVal === 'object' ? errVal.message : errVal) || `Request failed (${response.status}).`;
|
|
120
|
-
|
|
138
|
+
const error = new Error(message);
|
|
139
|
+
// #719: callers branch on the HTTP status (e.g. DELETE 404 already-removed vs
|
|
140
|
+
// 409 sequencing error) — carry it on the thrown error.
|
|
141
|
+
error.status = response.status;
|
|
142
|
+
throw error;
|
|
121
143
|
}
|
|
122
144
|
return payload;
|
|
123
145
|
}
|
|
@@ -444,16 +466,30 @@ function projectConversationPayload() {
|
|
|
444
466
|
}
|
|
445
467
|
|
|
446
468
|
function scheduleConversationDiskPersist() {
|
|
447
|
-
|
|
469
|
+
// Issue #708: no early-return on missing projectPath — manager/company scope buckets
|
|
470
|
+
// must persist even when no project is selected (their whole point is project-independence).
|
|
448
471
|
if (state.conversationPersistTimer) window.clearTimeout(state.conversationPersistTimer);
|
|
449
472
|
state.conversationPersistTimer = window.setTimeout(async () => {
|
|
450
473
|
state.conversationPersistTimer = null;
|
|
451
474
|
try {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
475
|
+
if (state.projectPath) {
|
|
476
|
+
await requestJson('/api/ai-hub/conversations', {
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
headers: { 'Content-Type': 'application/json' },
|
|
479
|
+
body: JSON.stringify(projectConversationPayload()),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
// Issue #708: persist scope buckets (manager/company) to their project-independent
|
|
483
|
+
// home so their runs survive project switches/removal and reload correctly.
|
|
484
|
+
for (const [scope, key] of [['manager', MANAGER_CONV_KEY], ['company', COMPANY_CONV_KEY]]) {
|
|
485
|
+
const bucket = state.conversations[key];
|
|
486
|
+
if (!Array.isArray(bucket)) continue;
|
|
487
|
+
await requestJson('/api/ai-hub/conversations', {
|
|
488
|
+
method: 'PUT',
|
|
489
|
+
headers: { 'Content-Type': 'application/json' },
|
|
490
|
+
body: JSON.stringify({ scope, activeId: null, conversations: bucket }),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
457
493
|
state.conversationDiskAvailable = true;
|
|
458
494
|
} catch (error) {
|
|
459
495
|
state.conversationDiskAvailable = false;
|
|
@@ -468,7 +504,28 @@ function persistConversations(options) {
|
|
|
468
504
|
}
|
|
469
505
|
|
|
470
506
|
async function hydrateConversationsFromServer() {
|
|
471
|
-
|
|
507
|
+
// Issue #708: load the project-independent scope buckets (manager/company) first, so
|
|
508
|
+
// manager runs are present regardless of whether a project is selected.
|
|
509
|
+
try {
|
|
510
|
+
for (const [scope, key] of [['manager', MANAGER_CONV_KEY], ['company', COMPANY_CONV_KEY]]) {
|
|
511
|
+
const scopePayload = await requestJson(`/api/ai-hub/conversations?scope=${scope}`);
|
|
512
|
+
const scopeConvs = Array.isArray(scopePayload.conversations) ? scopePayload.conversations : [];
|
|
513
|
+
for (const conv of scopeConvs) normalizeGeminiConversationMessages(conv);
|
|
514
|
+
// Only materialize a scope bucket when it has content (or already exists). Creating
|
|
515
|
+
// empty @manager/@company buckets on every project hydrate pollutes state.conversations
|
|
516
|
+
// and, because they land ahead of the project bucket, breaks the "project bucket is the
|
|
517
|
+
// primary key" invariant that project-scoped rendering relies on (#550 Watercooler).
|
|
518
|
+
if (scopeConvs.length || Array.isArray(state.conversations[key])) {
|
|
519
|
+
state.conversations[key] = scopeConvs;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.warn('Could not hydrate manager/company conversations:', error);
|
|
524
|
+
}
|
|
525
|
+
if (!state.projectPath) {
|
|
526
|
+
if (typeof tfRenderManager === 'function' && typeof tf !== 'undefined' && tf.area === 'manager') tfRenderManager();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
472
529
|
try {
|
|
473
530
|
const payload = await requestJson(`/api/ai-hub/conversations?projectPath=${encodeURIComponent(state.projectPath)}`);
|
|
474
531
|
const conversations = Array.isArray(payload.conversations) ? payload.conversations : [];
|
|
@@ -509,9 +566,10 @@ async function bgRefreshConversations() {
|
|
|
509
566
|
existing.push(conv);
|
|
510
567
|
changed = true;
|
|
511
568
|
} else {
|
|
512
|
-
// Update
|
|
569
|
+
// Update known conversations when the server projection changes. This
|
|
570
|
+
// includes job/persona recognition for running ad-hoc jobs.
|
|
513
571
|
const idx = existing.findIndex((c) => c.id === conv.id);
|
|
514
|
-
if (idx !== -1 && existing[idx]
|
|
572
|
+
if (idx !== -1 && conversationProjectionChanged(existing[idx], conv)) {
|
|
515
573
|
existing[idx] = conv;
|
|
516
574
|
changed = true;
|
|
517
575
|
}
|
|
@@ -533,7 +591,7 @@ function stopBgConvPoll() {
|
|
|
533
591
|
if (_bgConvPollHandle) { window.clearInterval(_bgConvPollHandle); _bgConvPollHandle = null; }
|
|
534
592
|
}
|
|
535
593
|
|
|
536
|
-
function
|
|
594
|
+
function needsDelegatedConversationRefresh() {
|
|
537
595
|
const active = activeConversation();
|
|
538
596
|
if (!active) return false;
|
|
539
597
|
if (isManagedDelegationChild(active) && active.status === 'running') return true;
|
|
@@ -548,12 +606,49 @@ function needsConversationRefresh() {
|
|
|
548
606
|
});
|
|
549
607
|
}
|
|
550
608
|
|
|
609
|
+
function needsConversationRefresh() {
|
|
610
|
+
return runningProjectConversations().length > 0 || needsDelegatedConversationRefresh();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function refreshRunningConversations() {
|
|
614
|
+
const running = runningProjectConversations();
|
|
615
|
+
if (!running.length) return false;
|
|
616
|
+
let changed = false;
|
|
617
|
+
for (const conv of running) {
|
|
618
|
+
try {
|
|
619
|
+
const run = await requestJson(`/api/ai-hub/runs/${encodeURIComponent(conv.runId)}`);
|
|
620
|
+
foldRunIntoConversation(conv, run);
|
|
621
|
+
upsertConversation(conv);
|
|
622
|
+
changed = true;
|
|
623
|
+
} catch (_) {
|
|
624
|
+
// The run may have just completed and left the in-memory registry before
|
|
625
|
+
// the conversation store hydrated. The next conversation refresh catches it.
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (changed) {
|
|
629
|
+
renderRail();
|
|
630
|
+
renderActive();
|
|
631
|
+
if (tf.area === 'company') tfRenderCompany();
|
|
632
|
+
else if (tf.area === 'manager') tfRenderManager();
|
|
633
|
+
}
|
|
634
|
+
return changed;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function refreshConversationProjections() {
|
|
638
|
+
await refreshRunningConversations();
|
|
639
|
+
if (needsDelegatedConversationRefresh()) {
|
|
640
|
+
await hydrateConversationsFromServer();
|
|
641
|
+
} else {
|
|
642
|
+
syncConversationRefreshPolling();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
551
646
|
function syncConversationRefreshPolling() {
|
|
552
647
|
const shouldPoll = needsConversationRefresh();
|
|
553
648
|
if (shouldPoll && !state.conversationRefreshHandle) {
|
|
554
649
|
state.conversationRefreshHandle = window.setInterval(() => {
|
|
555
|
-
|
|
556
|
-
console.warn('Could not refresh
|
|
650
|
+
refreshConversationProjections().catch((error) =>
|
|
651
|
+
console.warn('Could not refresh conversations:', error));
|
|
557
652
|
}, 1500);
|
|
558
653
|
} else if (!shouldPoll && state.conversationRefreshHandle) {
|
|
559
654
|
window.clearInterval(state.conversationRefreshHandle);
|
|
@@ -578,6 +673,32 @@ function projectConversations() {
|
|
|
578
673
|
return state.conversations[key] || [];
|
|
579
674
|
}
|
|
580
675
|
|
|
676
|
+
function conversationProjectionChanged(existing, incoming) {
|
|
677
|
+
if (!existing || !incoming) return true;
|
|
678
|
+
const fields = [
|
|
679
|
+
'jobId',
|
|
680
|
+
'jobTitle',
|
|
681
|
+
'personaKey',
|
|
682
|
+
'runId',
|
|
683
|
+
'sessionId',
|
|
684
|
+
'status',
|
|
685
|
+
'managedReviewStatus',
|
|
686
|
+
'compareRunId',
|
|
687
|
+
'sourceTrigger',
|
|
688
|
+
];
|
|
689
|
+
if (fields.some((field) => (existing[field] ?? null) !== (incoming[field] ?? null))) return true;
|
|
690
|
+
const existingMessages = Array.isArray(existing.messages) ? existing.messages.length : 0;
|
|
691
|
+
const incomingMessages = Array.isArray(incoming.messages) ? incoming.messages.length : 0;
|
|
692
|
+
if (existingMessages !== incomingMessages) return true;
|
|
693
|
+
const existingEvents = Array.isArray(existing.events) ? existing.events.length : 0;
|
|
694
|
+
const incomingEvents = Array.isArray(incoming.events) ? incoming.events.length : 0;
|
|
695
|
+
return existingEvents !== incomingEvents;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function runningProjectConversations() {
|
|
699
|
+
return projectConversations().filter((conv) => conv && conv.status === 'running' && conv.runId);
|
|
700
|
+
}
|
|
701
|
+
|
|
581
702
|
function setProjectConversations(list) {
|
|
582
703
|
const key = state.projectPath || '';
|
|
583
704
|
state.conversations[key] = list;
|
|
@@ -743,12 +864,15 @@ function syncConversationPanels(conv, switchedConv) {
|
|
|
743
864
|
}
|
|
744
865
|
|
|
745
866
|
function upsertConversation(conv) {
|
|
746
|
-
|
|
867
|
+
// Issue #708: route to the conversation's scope bucket (manager/company → sentinel
|
|
868
|
+
// key; project → its project bucket) so manager runs have a project-independent home.
|
|
869
|
+
const key = convBucketKey(conv);
|
|
870
|
+
const list = (state.conversations[key] || []).slice();
|
|
747
871
|
const idx = list.findIndex((c) => c.id === conv.id);
|
|
748
872
|
if (idx >= 0) list[idx] = conv;
|
|
749
873
|
else list.unshift(conv);
|
|
750
|
-
|
|
751
|
-
persistConversations();
|
|
874
|
+
state.conversations[key] = list;
|
|
875
|
+
persistConversations({ scope: convScope(conv) });
|
|
752
876
|
}
|
|
753
877
|
|
|
754
878
|
function newConversationId() {
|
|
@@ -854,7 +978,9 @@ function renderRail() {
|
|
|
854
978
|
const list = allProjectConversations.filter((conv) =>
|
|
855
979
|
(!state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
|
|
856
980
|
!isManagedDelegationChild(conv) &&
|
|
857
|
-
!AREA_SCOPED_JOBS.has(conv.jobId)
|
|
981
|
+
!AREA_SCOPED_JOBS.has(conv.jobId) &&
|
|
982
|
+
// #702/#708: manager/company-scoped runs surface on those tabs, not in a project.
|
|
983
|
+
convScope(conv) === 'project'
|
|
858
984
|
);
|
|
859
985
|
|
|
860
986
|
// Issue #550: Two-path routing for ad-hoc (freeform) conversations.
|
|
@@ -2034,11 +2160,29 @@ function renderTracker(conv) {
|
|
|
2034
2160
|
|
|
2035
2161
|
const stages = Array.isArray(conv.run.stages) ? conv.run.stages : [];
|
|
2036
2162
|
if (stages.length === 0) {
|
|
2037
|
-
|
|
2163
|
+
// Issue #711 — adhoc runs (jobId '__freeform__') declare no phases, so
|
|
2164
|
+
// there is no pizza tracker. We still surface the run totals (tokens,
|
|
2165
|
+
// cost, duration) that the server derives for every run. Keep the tracker
|
|
2166
|
+
// container visible in "totals-only" mode so renderTotals has a place to
|
|
2167
|
+
// render; otherwise hide the whole section.
|
|
2168
|
+
if (conv.run.totals) {
|
|
2169
|
+
tracker.hidden = false;
|
|
2170
|
+
tracker.classList.add('tracker--totals-only');
|
|
2171
|
+
tracker.classList.remove('tracker-compact');
|
|
2172
|
+
const rowsHost = els['tracker-rows'];
|
|
2173
|
+
if (rowsHost) rowsHost.innerHTML = '';
|
|
2174
|
+
const activeLabel = tracker.querySelector('.tracker-active-label');
|
|
2175
|
+
if (activeLabel) activeLabel.textContent = '';
|
|
2176
|
+
const note = els['tracker-note'];
|
|
2177
|
+
if (note) { note.hidden = true; note.textContent = ''; }
|
|
2178
|
+
} else {
|
|
2179
|
+
tracker.hidden = true;
|
|
2180
|
+
}
|
|
2038
2181
|
renderedTrackerKey = null;
|
|
2039
2182
|
return;
|
|
2040
2183
|
}
|
|
2041
2184
|
|
|
2185
|
+
tracker.classList.remove('tracker--totals-only');
|
|
2042
2186
|
tracker.hidden = false;
|
|
2043
2187
|
tracker.dataset.stageCount = String(stages.length);
|
|
2044
2188
|
const rowCapacity = trackerRowCapacity(tracker, stages);
|
|
@@ -4753,7 +4897,7 @@ function deriveTitle(jobTitle, instructions) {
|
|
|
4753
4897
|
// The browser sends raw manager instructions. AI Hub normalizes assigned
|
|
4754
4898
|
// FRAIM jobs into host-facing invocations so the Hub UI and channel callers
|
|
4755
4899
|
// share one start/continue contract.
|
|
4756
|
-
async function startRun(job, instructions, employeeId, preassignedConvId) {
|
|
4900
|
+
async function startRun(job, instructions, employeeId, preassignedConvId, invokedArea) {
|
|
4757
4901
|
const isFreeform = job.id === '__freeform__';
|
|
4758
4902
|
// In task-pane/extension mode: get a fresh selection snapshot and prepend
|
|
4759
4903
|
// document context to the instructions so the agent knows what the user is
|
|
@@ -4792,6 +4936,16 @@ async function startRun(job, instructions, employeeId, preassignedConvId) {
|
|
|
4792
4936
|
compareRun: null,
|
|
4793
4937
|
// Issue #489: capture selection state at job-start so write-back knows insert-after vs append.
|
|
4794
4938
|
wordStartedWithSelection: document.body.dataset.surface === 'task-pane' && !!(state.wordContext && state.wordContext.hasSelection),
|
|
4939
|
+
// Issue #702: remember which area this run was launched from so its run surfaces
|
|
4940
|
+
// where it was invoked (project-invoked → that project; manager-invoked → Manager tab).
|
|
4941
|
+
// Defaults to the current area.
|
|
4942
|
+
invokedArea: invokedArea || (typeof tf !== 'undefined' && tf.area) || 'projects',
|
|
4943
|
+
// Issue #708: persisted scope drives which store bucket the run lives in
|
|
4944
|
+
// (manager/company get a project-independent home). Derived from invocation area.
|
|
4945
|
+
scope: (function () {
|
|
4946
|
+
const a = invokedArea || (typeof tf !== 'undefined' && tf.area) || 'projects';
|
|
4947
|
+
return (a === 'manager' || a === 'company') ? a : 'project';
|
|
4948
|
+
})(),
|
|
4795
4949
|
};
|
|
4796
4950
|
upsertConversation(conv);
|
|
4797
4951
|
state.activeId = conv.id;
|
|
@@ -4958,6 +5112,18 @@ async function continueRun(text) {
|
|
|
4958
5112
|
}
|
|
4959
5113
|
|
|
4960
5114
|
function foldRunIntoConversation(conv, run) {
|
|
5115
|
+
// Issue #710: server run identity is canonical. The browser may have
|
|
5116
|
+
// created this conversation as provisional __freeform__ state, but every
|
|
5117
|
+
// server snapshot can promote it to a real catalog job.
|
|
5118
|
+
if (run.jobId) {
|
|
5119
|
+
conv.jobId = run.jobId;
|
|
5120
|
+
}
|
|
5121
|
+
if (run.jobTitle !== undefined && run.jobTitle !== null) {
|
|
5122
|
+
conv.jobTitle = run.jobTitle;
|
|
5123
|
+
}
|
|
5124
|
+
if (run.personaKey !== undefined) {
|
|
5125
|
+
conv.personaKey = run.personaKey;
|
|
5126
|
+
}
|
|
4961
5127
|
// Issue #347: keep the latest server snapshot under conv.run so the
|
|
4962
5128
|
// tracker / totals renderers can read it without re-doing work. Stages,
|
|
4963
5129
|
// currentPhase, phaseHistory, and totals are all server-derived per
|
|
@@ -4967,6 +5133,7 @@ function foldRunIntoConversation(conv, run) {
|
|
|
4967
5133
|
currentPhase: run.currentPhase || null,
|
|
4968
5134
|
phaseHistory: run.phaseHistory || [],
|
|
4969
5135
|
totals: run.totals || null,
|
|
5136
|
+
runDiscriminant: run.runDiscriminant || null,
|
|
4970
5137
|
};
|
|
4971
5138
|
// Replace the conversation's events with the run's events (single source of truth on the server).
|
|
4972
5139
|
// If this conversation was restarted on a different agent, prepend the prior run's events so
|
|
@@ -4984,10 +5151,6 @@ function foldRunIntoConversation(conv, run) {
|
|
|
4984
5151
|
conv.messages = priorMessages.length > 0 ? [...priorMessages, ...newMessages] : newMessages;
|
|
4985
5152
|
// Track session for resumption.
|
|
4986
5153
|
if (run.sessionId) conv.sessionId = run.sessionId;
|
|
4987
|
-
// R4: persist the persona key from the server-side run record.
|
|
4988
|
-
if (run.personaKey !== undefined && conv.personaKey == null) {
|
|
4989
|
-
conv.personaKey = run.personaKey;
|
|
4990
|
-
}
|
|
4991
5154
|
const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
|
|
4992
5155
|
if (runHandoff) {
|
|
4993
5156
|
conv.reviewHandoff = runHandoff;
|
|
@@ -6233,11 +6396,18 @@ function tfConvForAssignment(assignment) {
|
|
|
6233
6396
|
function tfConversationsForJob(jobId, opts) {
|
|
6234
6397
|
const out = [];
|
|
6235
6398
|
const projectPath = opts && opts.projectPath;
|
|
6399
|
+
// #702: optional invokedArea filter so the Manager rail shows only manager-invoked
|
|
6400
|
+
// runs of a persona job (project-invoked runs of the same job stay in the project).
|
|
6401
|
+
const invokedArea = opts && opts.invokedArea;
|
|
6236
6402
|
const lists = projectPath
|
|
6237
6403
|
? [state.conversations[projectPath] || []]
|
|
6238
6404
|
: Object.values(state.conversations || {});
|
|
6239
6405
|
for (const list of lists) {
|
|
6240
|
-
for (const c of (list || []))
|
|
6406
|
+
for (const c of (list || [])) {
|
|
6407
|
+
if (!c || c.jobId !== jobId) continue;
|
|
6408
|
+
if (invokedArea && (c.invokedArea || 'projects') !== invokedArea) continue;
|
|
6409
|
+
out.push(c);
|
|
6410
|
+
}
|
|
6241
6411
|
}
|
|
6242
6412
|
out.sort((a, b) => (b.updatedAt || b.createdAt || 0) - (a.updatedAt || a.createdAt || 0));
|
|
6243
6413
|
return out;
|
|
@@ -6497,6 +6667,9 @@ function tfRenderOverview() {
|
|
|
6497
6667
|
}
|
|
6498
6668
|
card.appendChild(team);
|
|
6499
6669
|
card.addEventListener('click', () => tfSelectProjectView('workspace', proj.id));
|
|
6670
|
+
// #719 R1/R1b: hover/focus-revealed remove control — Overview cards only, and
|
|
6671
|
+
// only while another project remains (the Hub always keeps at least one).
|
|
6672
|
+
if (tf.projects.length > 1) tfAttachProjectRemove(card, proj);
|
|
6500
6673
|
grid.appendChild(card);
|
|
6501
6674
|
}
|
|
6502
6675
|
const addCard = document.createElement('div');
|
|
@@ -6509,6 +6682,167 @@ function tfRenderOverview() {
|
|
|
6509
6682
|
tfSwitchOverviewView(storedView || (tf.projects.length >= 3 ? 'kanban' : 'cards'));
|
|
6510
6683
|
}
|
|
6511
6684
|
|
|
6685
|
+
// ---------------------------------------------------------------------------
|
|
6686
|
+
// Issue #719: remove a project from the Hub (Overview card 🗑 + inline confirm)
|
|
6687
|
+
// ---------------------------------------------------------------------------
|
|
6688
|
+
|
|
6689
|
+
// The card-positioned sibling of tfAttachRunDelete: a hover/focus-revealed 🗑 in
|
|
6690
|
+
// the card's top-right. Keyboard-operable (tabindex + Enter/Space) per R11.
|
|
6691
|
+
function tfAttachProjectRemove(cardEl, proj) {
|
|
6692
|
+
if (!cardEl || !proj) return;
|
|
6693
|
+
const del = document.createElement('span');
|
|
6694
|
+
del.className = 'proj-del';
|
|
6695
|
+
del.textContent = '🗑';
|
|
6696
|
+
del.title = 'Remove this project from the Hub';
|
|
6697
|
+
del.setAttribute('role', 'button');
|
|
6698
|
+
del.setAttribute('aria-label', 'Remove project from Hub');
|
|
6699
|
+
del.tabIndex = 0;
|
|
6700
|
+
del.addEventListener('click', (e) => {
|
|
6701
|
+
e.preventDefault();
|
|
6702
|
+
e.stopPropagation();
|
|
6703
|
+
tfConfirmProjectRemove(proj, cardEl);
|
|
6704
|
+
});
|
|
6705
|
+
del.addEventListener('keydown', (e) => {
|
|
6706
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
6707
|
+
e.preventDefault();
|
|
6708
|
+
e.stopPropagation();
|
|
6709
|
+
tfConfirmProjectRemove(proj, cardEl);
|
|
6710
|
+
});
|
|
6711
|
+
cardEl.appendChild(del);
|
|
6712
|
+
}
|
|
6713
|
+
|
|
6714
|
+
function tfCloseProjectConfirm() {
|
|
6715
|
+
document.querySelectorAll('.del-confirm').forEach((n) => n.remove());
|
|
6716
|
+
document.removeEventListener('keydown', tfProjectConfirmEscape, true);
|
|
6717
|
+
}
|
|
6718
|
+
function tfProjectConfirmEscape(e) {
|
|
6719
|
+
if (e.key === 'Escape') tfCloseProjectConfirm();
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
// Inline confirm anchored inside the card — the run-delete idiom (tfConfirmRunDelete)
|
|
6723
|
+
// with the project scope-of-loss copy. Loads run/schedule state from the server so
|
|
6724
|
+
// the copy can state the schedule count (R6) and block removal while runs are in
|
|
6725
|
+
// progress (R7). Nothing is deleted until the confirm button is clicked (C1).
|
|
6726
|
+
function tfConfirmProjectRemove(proj, cardEl) {
|
|
6727
|
+
tfCloseProjectConfirm();
|
|
6728
|
+
const box = document.createElement('div');
|
|
6729
|
+
box.className = 'del-confirm';
|
|
6730
|
+
// Any click inside the confirm must not bubble into the card's open-workspace handler.
|
|
6731
|
+
box.addEventListener('click', (e) => e.stopPropagation());
|
|
6732
|
+
const msg = document.createElement('div');
|
|
6733
|
+
msg.textContent = 'Remove "' + (proj.name || 'this project') + '" from the Hub? Its run history, job assignments, and Hub settings are forgotten. The project’s files on disk are untouched.';
|
|
6734
|
+
const detail = document.createElement('div');
|
|
6735
|
+
detail.className = 'dc-detail';
|
|
6736
|
+
detail.textContent = 'Checking for runs and schedules…';
|
|
6737
|
+
const row = document.createElement('div'); row.className = 'dc-row';
|
|
6738
|
+
const del = document.createElement('button'); del.className = 'dc-del'; del.type = 'button'; del.textContent = 'Remove project';
|
|
6739
|
+
del.disabled = true; // enabled once the run/schedule check resolves (C1/R7)
|
|
6740
|
+
del.addEventListener('click', (e) => {
|
|
6741
|
+
e.stopPropagation();
|
|
6742
|
+
if (del.disabled) return;
|
|
6743
|
+
tfCloseProjectConfirm();
|
|
6744
|
+
tfRemoveProject(proj);
|
|
6745
|
+
});
|
|
6746
|
+
const cancel = document.createElement('button'); cancel.className = 'dc-cancel'; cancel.type = 'button'; cancel.textContent = 'Cancel';
|
|
6747
|
+
cancel.addEventListener('click', (e) => { e.stopPropagation(); tfCloseProjectConfirm(); });
|
|
6748
|
+
row.appendChild(del); row.appendChild(cancel);
|
|
6749
|
+
box.appendChild(msg); box.appendChild(detail); box.appendChild(row);
|
|
6750
|
+
cardEl.appendChild(box);
|
|
6751
|
+
document.addEventListener('keydown', tfProjectConfirmEscape, true);
|
|
6752
|
+
|
|
6753
|
+
const encodedPath = encodeURIComponent(proj.folderPath || '');
|
|
6754
|
+
Promise.all([
|
|
6755
|
+
requestJson('/api/ai-hub/conversations?projectPath=' + encodedPath).catch(() => null),
|
|
6756
|
+
requestJson('/api/ai-hub/schedules?projectPath=' + encodedPath).catch(() => []),
|
|
6757
|
+
requestJson('/api/ai-hub/webhooks?projectPath=' + encodedPath).catch(() => []),
|
|
6758
|
+
]).then(([convPayload, schedules, webhooks]) => {
|
|
6759
|
+
if (!box.isConnected) return;
|
|
6760
|
+
const serverConvs = (convPayload && Array.isArray(convPayload.conversations)) ? convPayload.conversations : [];
|
|
6761
|
+
const cachedConvs = state.conversations[proj.folderPath] || [];
|
|
6762
|
+
const running = [...serverConvs, ...cachedConvs].some((c) => c && c.status === 'running');
|
|
6763
|
+
if (running) {
|
|
6764
|
+
// R7: never forget in-flight work — block until the runs finish or are stopped.
|
|
6765
|
+
detail.textContent = 'Runs are in progress in this project. Stop them or wait for them to finish before removing.';
|
|
6766
|
+
del.disabled = true;
|
|
6767
|
+
return;
|
|
6768
|
+
}
|
|
6769
|
+
const scheduleCount = Array.isArray(schedules) ? schedules.length : 0;
|
|
6770
|
+
const webhookCount = Array.isArray(webhooks) ? webhooks.length : 0;
|
|
6771
|
+
const parts = [];
|
|
6772
|
+
if (scheduleCount) parts.push(scheduleCount + ' scheduled deployment' + (scheduleCount === 1 ? '' : 's'));
|
|
6773
|
+
if (webhookCount) parts.push(webhookCount + ' webhook' + (webhookCount === 1 ? '' : 's'));
|
|
6774
|
+
detail.textContent = parts.length ? (parts.join(' and ') + ' will also be removed.') : '';
|
|
6775
|
+
if (!parts.length) detail.remove();
|
|
6776
|
+
del.disabled = false;
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
// R10: adopt a server projects list as the authoritative tf.projects — a REPLACE,
|
|
6781
|
+
// not a tfMergeProjects union, so a later tfPersistProjects PUT cannot re-add a
|
|
6782
|
+
// removed project from stale in-memory state.
|
|
6783
|
+
function tfAdoptServerProjects(projects) {
|
|
6784
|
+
const normalized = (Array.isArray(projects) ? projects : [])
|
|
6785
|
+
.map((project) => tfNormalizeProject(project))
|
|
6786
|
+
.filter(Boolean);
|
|
6787
|
+
tf.projects = tfApplyUniqueProjectIds(normalized);
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
async function tfRemoveProject(proj) {
|
|
6791
|
+
const removedCanonical = tfCanonicalProjectPath(proj.folderPath || '');
|
|
6792
|
+
// R5/D4: never delete the workspace we are standing in — the server re-injects
|
|
6793
|
+
// the current projectPath on every load, so switch to another project first.
|
|
6794
|
+
if (removedCanonical === tfCanonicalProjectPath(state.projectPath)) {
|
|
6795
|
+
const next = tf.projects.find((p) => p.id !== proj.id && tfCanonicalProjectPath(p.folderPath || '') !== removedCanonical);
|
|
6796
|
+
if (!next) { showStatus('Cannot remove the only project in the Hub.', true); return; }
|
|
6797
|
+
try {
|
|
6798
|
+
await tfSwitchProjectFolder(next.folderPath);
|
|
6799
|
+
tf.activeProjectId = next.id;
|
|
6800
|
+
} catch (e) {
|
|
6801
|
+
showStatus('Could not switch to another project first; nothing was removed.', true);
|
|
6802
|
+
return;
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6806
|
+
let payload;
|
|
6807
|
+
try {
|
|
6808
|
+
payload = await requestJson(
|
|
6809
|
+
'/api/ai-hub/projects/' + encodeURIComponent(proj.id) + '?projectPath=' + encodeURIComponent(state.projectPath),
|
|
6810
|
+
{ method: 'DELETE' },
|
|
6811
|
+
);
|
|
6812
|
+
} catch (error) {
|
|
6813
|
+
if (error && error.status === 404) {
|
|
6814
|
+
// Already removed elsewhere (double-click / second tab): treat as success
|
|
6815
|
+
// for rendering and adopt the server's current list.
|
|
6816
|
+
try { payload = { projects: (await requestJson('/api/ai-hub/projects?projectPath=' + encodeURIComponent(state.projectPath))).projects }; }
|
|
6817
|
+
catch { payload = { projects: tf.projects.filter((p) => p.id !== proj.id) }; }
|
|
6818
|
+
showStatus('That project was already removed.', false);
|
|
6819
|
+
} else if (error && error.status === 409) {
|
|
6820
|
+
showStatus('Switch to another project first, then remove this one.', true);
|
|
6821
|
+
return;
|
|
6822
|
+
} else {
|
|
6823
|
+
// Server unreachable or rejected: no optimistic client mutation.
|
|
6824
|
+
showStatus('Could not remove the project: ' + (error && error.message ? error.message : 'request failed.'), true);
|
|
6825
|
+
return;
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
// R3/R10: drop every client layer, adopting the server list as authoritative.
|
|
6830
|
+
tfAdoptServerProjects(payload && payload.projects);
|
|
6831
|
+
delete tf.assignments[proj.id];
|
|
6832
|
+
tfPersistAssignments();
|
|
6833
|
+
for (const key of Object.keys(state.conversations || {})) {
|
|
6834
|
+
if (tfCanonicalProjectPath(key) === removedCanonical) delete state.conversations[key];
|
|
6835
|
+
}
|
|
6836
|
+
if (!tf.projects.some((p) => p.id === tf.activeProjectId)) {
|
|
6837
|
+
tf.activeProjectId = tf.projects[0] ? tf.projects[0].id : null;
|
|
6838
|
+
}
|
|
6839
|
+
tfPersistProjects();
|
|
6840
|
+
tfRenderProjectTabs();
|
|
6841
|
+
tfRenderOverview();
|
|
6842
|
+
if (typeof renderRail === 'function') renderRail();
|
|
6843
|
+
showStatus('"' + (proj.name || 'Project') + '" was removed from the Hub. Its files on disk are untouched.', false);
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6512
6846
|
// ---------------------------------------------------------------------------
|
|
6513
6847
|
// Issue #671: Kanban view for Projects Overview
|
|
6514
6848
|
// ---------------------------------------------------------------------------
|
|
@@ -8009,10 +8343,12 @@ function tfActiveOrgConv() {
|
|
|
8009
8343
|
);
|
|
8010
8344
|
}
|
|
8011
8345
|
|
|
8012
|
-
// #594 R2: return the
|
|
8346
|
+
// #594 R2: return the manager-scoped conversation to show in Manager area.
|
|
8347
|
+
// #702: manager-tab runs are those invoked from the manager area (any persona job),
|
|
8348
|
+
// plus the area-only manager-agreements job (older runs predate invokedArea). An active
|
|
8349
|
+
// manager-invoked run surfaces in the Manager conversation host (Coach panel included).
|
|
8013
8350
|
function tfActiveMgrConv() {
|
|
8014
|
-
const
|
|
8015
|
-
const convs = Object.values(state.conversations || {}).flat().filter((c) => mgrJobs.has(c.jobId));
|
|
8351
|
+
const convs = Object.values(state.conversations || {}).flat().filter((c) => c && (convScope(c) === 'manager' || c.jobId === 'manager-agreements'));
|
|
8016
8352
|
return (
|
|
8017
8353
|
convs.find((c) => c.status === 'running') ||
|
|
8018
8354
|
convs.find((c) => c.status === 'waiting') ||
|
|
@@ -8021,6 +8357,93 @@ function tfActiveMgrConv() {
|
|
|
8021
8357
|
);
|
|
8022
8358
|
}
|
|
8023
8359
|
|
|
8360
|
+
// #702: is a persona hired (has a company seat) so its manager-tab jobs should show?
|
|
8361
|
+
// (MANAGER_ASHLEY_JOBS is defined once at the top of this file as the single source of truth.)
|
|
8362
|
+
function tfIsPersonaHired(personaKey) {
|
|
8363
|
+
const personas = (state.bootstrap && state.bootstrap.personas) || [];
|
|
8364
|
+
const p = personas.find((x) => x && x.key === personaKey);
|
|
8365
|
+
return !!(p && (p.status === 'hired' || (p.seatCount || 0) > 0));
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
// #702: launch a persona job from the Manager tab. Reuses the area pre-flight modal +
|
|
8369
|
+
// run routing (targetArea 'manager'), so the run opens in #manager-conv-host with the
|
|
8370
|
+
// Coach panel and is tagged invokedArea='manager' — exactly like manager-agreements.
|
|
8371
|
+
function tfStartManagerPersonaJob(jobId) {
|
|
8372
|
+
tfOpenAreaOnboardModal(jobId, 'Run ' + jobId + ' across the projects I manage and my calendar.', 'manager');
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
// #702: run-item button matching the Projects rail (conv-item), for the manager
|
|
8376
|
+
// employee group. Mirrors the core of renderRail's buildRunButton so runs look
|
|
8377
|
+
// identical on both tabs.
|
|
8378
|
+
function tfBuildManagerRunItem(conv) {
|
|
8379
|
+
const btn = document.createElement('button');
|
|
8380
|
+
btn.className = 'conv-item';
|
|
8381
|
+
btn.type = 'button';
|
|
8382
|
+
btn.dataset.conv = conv.id;
|
|
8383
|
+
const body = document.createElement('span'); body.className = 'conv-body';
|
|
8384
|
+
const title = document.createElement('span'); title.className = 'conv-title';
|
|
8385
|
+
title.textContent = conv.title || conv.jobTitle || conv.jobId || 'Run';
|
|
8386
|
+
body.appendChild(title); btn.appendChild(body);
|
|
8387
|
+
const dotClass = conversationStateDotClass(conv);
|
|
8388
|
+
const dot = document.createElement('span'); dot.className = 'state-dot conv-state-dot dot-' + dotClass;
|
|
8389
|
+
if (typeof tfDotTitle === 'function') dot.title = tfDotTitle(dotClass);
|
|
8390
|
+
btn.appendChild(dot);
|
|
8391
|
+
const status = document.createElement('span'); status.className = 'conv-status';
|
|
8392
|
+
status.textContent = conversationStateLabel(conv);
|
|
8393
|
+
status.classList.add(conversationUiState(conv));
|
|
8394
|
+
btn.appendChild(status);
|
|
8395
|
+
tfAttachRunDelete(btn, conv, { tree: false });
|
|
8396
|
+
btn.addEventListener('click', () => switchToConversation(conv.id));
|
|
8397
|
+
return btn;
|
|
8398
|
+
}
|
|
8399
|
+
|
|
8400
|
+
// #702: render a hired persona as an employee group on the Manager rail — the SAME
|
|
8401
|
+
// visual as the Projects rail employee groups (avatar + name + role + count + "+" that
|
|
8402
|
+
// opens the job palette), with the persona's MANAGER-invoked runs listed under it.
|
|
8403
|
+
// Keeps a persona looking like an employee, consistent across tabs.
|
|
8404
|
+
function tfRenderManagerPersonaGroup(rail, persona) {
|
|
8405
|
+
const sample = { personaKey: persona.key };
|
|
8406
|
+
const runs = Object.values(state.conversations || {}).flat()
|
|
8407
|
+
.filter((c) => c && c.personaKey === persona.key && convScope(c) === 'manager')
|
|
8408
|
+
.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
|
8409
|
+
const details = document.createElement('details');
|
|
8410
|
+
details.className = 'conv-employee-group';
|
|
8411
|
+
details.open = true;
|
|
8412
|
+
const summary = document.createElement('summary');
|
|
8413
|
+
summary.className = 'conv-employee-tab';
|
|
8414
|
+
const avatar = buildConversationEmployeeAvatar(sample, 'conv-employee-avatar');
|
|
8415
|
+
const copy = document.createElement('span'); copy.className = 'conv-employee-tab-copy';
|
|
8416
|
+
const label = document.createElement('strong'); label.className = 'conv-employee-tab-label';
|
|
8417
|
+
label.textContent = persona.displayName || persona.key;
|
|
8418
|
+
const detail = document.createElement('small'); detail.className = 'conv-employee-tab-detail';
|
|
8419
|
+
detail.textContent = persona.role || 'AI Employee';
|
|
8420
|
+
copy.appendChild(label); copy.appendChild(detail);
|
|
8421
|
+
const addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.className = 'conv-employee-add';
|
|
8422
|
+
addBtn.textContent = '+';
|
|
8423
|
+
addBtn.title = 'Launch a job for ' + (persona.displayName || persona.key);
|
|
8424
|
+
addBtn.setAttribute('aria-label', 'Launch a job for ' + (persona.displayName || persona.key));
|
|
8425
|
+
addBtn.addEventListener('click', (e) => {
|
|
8426
|
+
e.preventDefault(); e.stopPropagation();
|
|
8427
|
+
// Launch from the Manager tab: palette prefiltered to this persona. Runs started
|
|
8428
|
+
// while the manager tab is active default to invokedArea='manager'.
|
|
8429
|
+
openPalette({ employeeId: (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.employeeId) || 'claude', prefixSearch: '/' + persona.key });
|
|
8430
|
+
});
|
|
8431
|
+
const count = document.createElement('span'); count.className = 'conv-employee-tab-count';
|
|
8432
|
+
count.textContent = String(runs.length);
|
|
8433
|
+
summary.appendChild(avatar); summary.appendChild(copy); summary.appendChild(addBtn); summary.appendChild(count);
|
|
8434
|
+
details.appendChild(summary);
|
|
8435
|
+
const listDiv = document.createElement('div'); listDiv.className = 'conv-employee-list';
|
|
8436
|
+
if (!runs.length) {
|
|
8437
|
+
const hint = document.createElement('div'); hint.className = 'eh-empty';
|
|
8438
|
+
hint.textContent = 'No runs yet — + to launch a job.';
|
|
8439
|
+
listDiv.appendChild(hint);
|
|
8440
|
+
} else {
|
|
8441
|
+
for (const c of runs) listDiv.appendChild(tfBuildManagerRunItem(c));
|
|
8442
|
+
}
|
|
8443
|
+
details.appendChild(listDiv);
|
|
8444
|
+
rail.appendChild(details);
|
|
8445
|
+
}
|
|
8446
|
+
|
|
8024
8447
|
// Move the shared .page conversation panel into the specified area's workspace-conv host.
|
|
8025
8448
|
// All .workspace-conv CSS rules (header/rail hidden, conv fills space) apply automatically
|
|
8026
8449
|
// because the area-conv-host elements carry the workspace-conv class.
|
|
@@ -8216,6 +8639,30 @@ function tfRenderManager() {
|
|
|
8216
8639
|
rail.appendChild(infoBtn);
|
|
8217
8640
|
// #693 R1 (PR round 2): the "Manager jobs" launcher list is retired.
|
|
8218
8641
|
// Manager agreements now runs from the "Context & rules" section it populates.
|
|
8642
|
+
// Issue #702: for each hired persona with catalog jobs, render an EMPLOYEE GROUP on
|
|
8643
|
+
// the Manager rail — the same avatar + name + runs + "+" presentation as the Projects
|
|
8644
|
+
// rail (tfRenderManagerPersonaGroup reuses conv-employee-group), so a persona looks
|
|
8645
|
+
// identical on both tabs. Data-driven from bootstrap job metadata (job.requiredPersonaKey),
|
|
8646
|
+
// not a hardcoded list. Runs launched here are tagged invokedArea='manager'; the same
|
|
8647
|
+
// employee's job launched inside a project stays in that project.
|
|
8648
|
+
const mgrPersonas = (state.bootstrap && state.bootstrap.personas) || [];
|
|
8649
|
+
const catalogJobs = (state.bootstrap && state.bootstrap.jobs) || [];
|
|
8650
|
+
let anyMgrEmployee = false;
|
|
8651
|
+
for (const persona of mgrPersonas) {
|
|
8652
|
+
if (!tfIsPersonaHired(persona.key)) continue;
|
|
8653
|
+
if (!catalogJobs.some((j) => j && j.requiredPersonaKey === persona.key)) continue;
|
|
8654
|
+
if (!anyMgrEmployee) {
|
|
8655
|
+
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Your employees';
|
|
8656
|
+
rail.appendChild(head);
|
|
8657
|
+
anyMgrEmployee = true;
|
|
8658
|
+
}
|
|
8659
|
+
tfRenderManagerPersonaGroup(rail, persona);
|
|
8660
|
+
}
|
|
8661
|
+
if (anyMgrEmployee) {
|
|
8662
|
+
const pNote = document.createElement('div'); pNote.className = 'area-rail-note';
|
|
8663
|
+
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.";
|
|
8664
|
+
rail.appendChild(pNote);
|
|
8665
|
+
}
|
|
8219
8666
|
}
|
|
8220
8667
|
const profile = document.getElementById('manager-profile');
|
|
8221
8668
|
const learn = document.getElementById('manager-learnings');
|
|
@@ -8847,6 +9294,15 @@ const AREA_ONBOARD_CONFIG = {
|
|
|
8847
9294
|
title: 'Manager agreements',
|
|
8848
9295
|
desc: 'FRAIM will learn how you work and write manager_context.md and manager_rules.md. Add any specific direction below.',
|
|
8849
9296
|
},
|
|
9297
|
+
// Issue #702: Ashley's manager-tab jobs.
|
|
9298
|
+
'executive-assistant': {
|
|
9299
|
+
title: 'Ashley — what needs you today',
|
|
9300
|
+
desc: "Ashley reviews the projects you manage plus your calendar and briefs you on what needs your attention. Add any specific focus below.",
|
|
9301
|
+
},
|
|
9302
|
+
'weekly-operating-review': {
|
|
9303
|
+
title: 'Ashley — weekly operating review',
|
|
9304
|
+
desc: 'Ashley produces your weekly portfolio operating brief. Add any specific focus areas below.',
|
|
9305
|
+
},
|
|
8850
9306
|
};
|
|
8851
9307
|
|
|
8852
9308
|
function tfOpenAreaOnboardModal(jobId, baseMessage, area) {
|
|
@@ -8892,7 +9348,7 @@ async function tfStartOnboardingJob(jobId, message, targetArea, userContext) {
|
|
|
8892
9348
|
const finalMessage = (userContext && userContext.trim()) ? userContext.trim() : jobMessage;
|
|
8893
9349
|
if (job && typeof startRun === 'function') {
|
|
8894
9350
|
if (targetArea) tfShowArea(targetArea);
|
|
8895
|
-
await startRun(job, finalMessage, employeeId);
|
|
9351
|
+
await startRun(job, finalMessage, employeeId, undefined, targetArea);
|
|
8896
9352
|
// After run creation refresh the area panel so the conversation becomes visible.
|
|
8897
9353
|
if (targetArea === 'company') tfRenderCompany();
|
|
8898
9354
|
else if (targetArea === 'manager') tfRenderManager();
|