fraim-hub 2.0.335 → 2.0.337
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/attachment-store.js +35 -2
- package/dist/src/ai-hub/desktop-main.js +5 -0
- package/dist/src/ai-hub/native-voice-input.js +141 -0
- package/dist/src/ai-hub/server.js +29 -2
- package/package.json +2 -2
- package/public/ai-hub/index.html +26 -16
- package/public/ai-hub/script.js +282 -455
- package/public/ai-hub/styles.css +47 -115
package/public/ai-hub/script.js
CHANGED
|
@@ -144,7 +144,7 @@ function gatherElements() {
|
|
|
144
144
|
'coach-text', 'send', 'micro-manage', 'micro-log', 'resume-command', 'resume-command-code', 'resume-command-copy',
|
|
145
145
|
'coach-note',
|
|
146
146
|
// Issue #1651: composer attachments.
|
|
147
|
-
'coach', 'composer-dropzone', 'staged-attachments', 'attach-btn', 'attach-file-input',
|
|
147
|
+
'coach', 'composer-dropzone', 'staged-attachments', 'attach-btn', 'attach-file-input', 'voice-input-btn', 'voice-input-status',
|
|
148
148
|
'thread-panel', 'thread-panel-kicker', 'micro-manage-toggle', 'micro-return', 'micro-activity-state',
|
|
149
149
|
'quick-coach-btns',
|
|
150
150
|
'positive-reinforcement-btn', 'positive-reinforcement-dropdown',
|
|
@@ -332,6 +332,7 @@ function mergePersonaProjection(incoming, previous) {
|
|
|
332
332
|
function applyBootstrap(bootstrap, docUrl) {
|
|
333
333
|
bootstrap = mergePersonaProjection(bootstrap, state.bootstrap);
|
|
334
334
|
state.bootstrap = bootstrap;
|
|
335
|
+
syncVoiceInput();
|
|
335
336
|
state.projectPath = bootstrap.project.path;
|
|
336
337
|
// Issue #1044: seed client-side tombstone set from server preferences so
|
|
337
338
|
// tfEnsureCurrentProject does not re-inject a path the user just deleted.
|
|
@@ -2164,6 +2165,64 @@ function syncQuickCoachButtons(conv) {
|
|
|
2164
2165
|
}
|
|
2165
2166
|
}
|
|
2166
2167
|
|
|
2168
|
+
let voiceInputBusy = false;
|
|
2169
|
+
|
|
2170
|
+
function syncVoiceInput() {
|
|
2171
|
+
const button = els['voice-input-btn'];
|
|
2172
|
+
if (!button) return;
|
|
2173
|
+
const capability = state.bootstrap && state.bootstrap.voiceInput;
|
|
2174
|
+
const unsupported = !capability || capability.mode === 'unsupported';
|
|
2175
|
+
button.disabled = voiceInputBusy || unsupported || !!els['coach-text'].disabled;
|
|
2176
|
+
button.setAttribute('aria-busy', String(voiceInputBusy));
|
|
2177
|
+
const label = unsupported
|
|
2178
|
+
? (capability && capability.guidance) || 'Voice input is unavailable. Reload the Hub to check again.'
|
|
2179
|
+
: capability.mode === 'manual' ? capability.guidance : 'Start voice input using system dictation';
|
|
2180
|
+
button.title = label;
|
|
2181
|
+
button.setAttribute('aria-label', label);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function showVoiceInputStatus(message, failed = false) {
|
|
2185
|
+
const status = els['voice-input-status'];
|
|
2186
|
+
status.textContent = message;
|
|
2187
|
+
status.hidden = false;
|
|
2188
|
+
status.classList.toggle('voice-input-status--error', failed);
|
|
2189
|
+
// Narrow layouts scroll the whole pane. Keep both recovery text and actions
|
|
2190
|
+
// visible when a new notice increases the composer's height, without refocusing.
|
|
2191
|
+
status.closest('.coach-input').scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
async function startVoiceInput() {
|
|
2195
|
+
const capability = state.bootstrap && state.bootstrap.voiceInput;
|
|
2196
|
+
const textarea = els['coach-text'];
|
|
2197
|
+
if (voiceInputBusy || !capability || capability.mode === 'unsupported' || textarea.disabled) return;
|
|
2198
|
+
const { selectionStart, selectionEnd, selectionDirection } = textarea;
|
|
2199
|
+
textarea.focus({ preventScroll: true });
|
|
2200
|
+
textarea.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
|
|
2201
|
+
if (capability.mode === 'manual') {
|
|
2202
|
+
showVoiceInputStatus(capability.guidance);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
voiceInputBusy = true;
|
|
2206
|
+
syncVoiceInput();
|
|
2207
|
+
showVoiceInputStatus('Requesting system dictation...');
|
|
2208
|
+
try {
|
|
2209
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
2210
|
+
// Do not launch an OS input action if the manager moved to another control.
|
|
2211
|
+
if (document.activeElement !== textarea) {
|
|
2212
|
+
showVoiceInputStatus('Voice input canceled because the composer lost focus. Try again.');
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const result = await requestJson('/api/ai-hub/voice-input/start', { method: 'POST' }, 7000);
|
|
2216
|
+
showVoiceInputStatus(result.message);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const message = error.message || 'Could not request system dictation.';
|
|
2219
|
+
showVoiceInputStatus(message.includes(capability.guidance) ? message : `${message} ${capability.guidance}`, true);
|
|
2220
|
+
} finally {
|
|
2221
|
+
voiceInputBusy = false;
|
|
2222
|
+
syncVoiceInput();
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2167
2226
|
function composerIsArmed() {
|
|
2168
2227
|
return !!(
|
|
2169
2228
|
(els['coach-text'] && els['coach-text'].value.trim().length > 0) ||
|
|
@@ -2688,8 +2747,7 @@ function showCustomEmployeeDeleteConfirm(group, detailsEl) {
|
|
|
2688
2747
|
// ---------------------------------------------------------------------------
|
|
2689
2748
|
|
|
2690
2749
|
// Build a run-item button for any area rail.
|
|
2691
|
-
//
|
|
2692
|
-
// and tfRenderAreaPersonaGroup(). Extracted from renderRail's inner buildRunButton.
|
|
2750
|
+
// Used by the shared employee builder for Projects, Manager, and Company.
|
|
2693
2751
|
function buildAreaRunButton(conv, options) {
|
|
2694
2752
|
options = options || {};
|
|
2695
2753
|
const btn = document.createElement('button');
|
|
@@ -2753,7 +2811,7 @@ function buildAreaRunButton(conv, options) {
|
|
|
2753
2811
|
}
|
|
2754
2812
|
|
|
2755
2813
|
// Build a <div class="conv-employee-list"> with run buttons and optional delegation children.
|
|
2756
|
-
// childrenByManager: Map<id, conv[]> —
|
|
2814
|
+
// childrenByManager: Map<id, conv[]> — scoped by the caller before nesting.
|
|
2757
2815
|
function buildAreaGroupList(conversations, opts, childrenByManager) {
|
|
2758
2816
|
childrenByManager = childrenByManager || new Map();
|
|
2759
2817
|
opts = opts || {};
|
|
@@ -2845,19 +2903,21 @@ function appendEmployeeGroupWithActions(container, details) {
|
|
|
2845
2903
|
|
|
2846
2904
|
function renderRail() {
|
|
2847
2905
|
renderTeamRoster();
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2906
|
+
renderScopeEmployees(els['conv-list'], 'projects');
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// All scopes share grouping, employee actions, nested workstreams and poll state.
|
|
2910
|
+
function renderScopeEmployees(host, area) {
|
|
2911
|
+
if (!host) return;
|
|
2912
|
+
const isProject = area === 'projects';
|
|
2913
|
+
const activeRailProjectKey = isProject ? (tf.activeProjectId || state.projectPath || 'current') : area;
|
|
2851
2914
|
const employeeGroupOpenState = new Map();
|
|
2852
|
-
|
|
2915
|
+
host.querySelectorAll('details.conv-employee-group[data-employee-key]').forEach((details) => {
|
|
2853
2916
|
const key = details.dataset ? details.dataset.employeeKey : '';
|
|
2854
2917
|
const projectKey = details.dataset ? (details.dataset.projectKey || activeRailProjectKey) : activeRailProjectKey;
|
|
2855
|
-
//
|
|
2856
|
-
//
|
|
2857
|
-
|
|
2858
|
-
// groups always follow the default (collapsed); with-run groups default open (#726 UX).
|
|
2859
|
-
const hasRuns = !!details.querySelector('.conv-item');
|
|
2860
|
-
if (key && hasRuns) employeeGroupOpenState.set(projectKey + ':' + key, details.open);
|
|
2918
|
+
// History groups default closed whether hydrated or empty. Preserve every
|
|
2919
|
+
// existing group, including an explicitly collapsed empty In Progress group.
|
|
2920
|
+
if (key) employeeGroupOpenState.set(projectKey + ':' + key, details.open);
|
|
2861
2921
|
});
|
|
2862
2922
|
const employeeGroupStateKey = (key) => activeRailProjectKey + ':' + key;
|
|
2863
2923
|
// Preserve the user's open/closed toggle across re-renders; otherwise fall back to
|
|
@@ -2868,13 +2928,15 @@ function renderRail() {
|
|
|
2868
2928
|
? employeeGroupOpenState.get(employeeGroupStateKey(key))
|
|
2869
2929
|
: defaultOpen
|
|
2870
2930
|
);
|
|
2871
|
-
|
|
2931
|
+
host.innerHTML = '';
|
|
2872
2932
|
// R4: filter by selected persona when one is active.
|
|
2873
2933
|
// #693 R1: the "Project Updates" section was retired; project-lifecycle runs
|
|
2874
2934
|
// (project-onboarding + sleep-on-learnings) now surface in the Runs list under
|
|
2875
2935
|
// their employee like any other run, so their history stays reachable.
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2936
|
+
const scopedConversations = isProject
|
|
2937
|
+
? projectConversations().filter((conv) => convScope(conv) === 'project')
|
|
2938
|
+
: tfAreaScopedConversations(area);
|
|
2939
|
+
const managedChildren = scopedConversations.filter(isManagedDelegationChild);
|
|
2878
2940
|
const childrenByManager = new Map();
|
|
2879
2941
|
const childIdentity = (child) => child && (child.id || child.runId);
|
|
2880
2942
|
const addChildForManager = (managerId, child) => {
|
|
@@ -2897,7 +2959,7 @@ function renderRail() {
|
|
|
2897
2959
|
if (task.jobId && child.jobId && task.jobId !== child.jobId) return false;
|
|
2898
2960
|
return true;
|
|
2899
2961
|
};
|
|
2900
|
-
for (const manager of
|
|
2962
|
+
for (const manager of scopedConversations) {
|
|
2901
2963
|
const ledger = normalizeDelegationLedger(manager.delegation);
|
|
2902
2964
|
if (!ledger) continue;
|
|
2903
2965
|
for (const task of ledger.tasks || []) {
|
|
@@ -2908,12 +2970,12 @@ function renderRail() {
|
|
|
2908
2970
|
}
|
|
2909
2971
|
}
|
|
2910
2972
|
}
|
|
2911
|
-
const list =
|
|
2912
|
-
(!state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
|
|
2973
|
+
const list = scopedConversations.filter((conv) =>
|
|
2974
|
+
(!isProject || !state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
|
|
2913
2975
|
!isManagedDelegationChild(conv) &&
|
|
2914
2976
|
// #702/#708/#1408: manager/company-scoped runs surface on those tabs, not in a
|
|
2915
2977
|
// project, decided by scope alone — no job id is ever excluded from Projects.
|
|
2916
|
-
convScope(conv) === 'project'
|
|
2978
|
+
convScope(conv) === (isProject ? 'project' : area)
|
|
2917
2979
|
);
|
|
2918
2980
|
|
|
2919
2981
|
// Issue #851 Option A: In Progress section (top) + per-employee accordion tree.
|
|
@@ -2975,13 +3037,15 @@ function renderRail() {
|
|
|
2975
3037
|
...managerTeamKeys,
|
|
2976
3038
|
]);
|
|
2977
3039
|
// Apply persona filter.
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
|
|
3040
|
+
const areaPersonas = isProject ? [] : tfAreaRailPersonas(area);
|
|
3041
|
+
const areaPersonaOrder = new Map(areaPersonas.map((persona, index) => [persona.key, index]));
|
|
3042
|
+
const filteredEmployeeKeys = isProject
|
|
3043
|
+
? (state.selectedPersonaKey ? new Set([state.selectedPersonaKey].filter((k) => projectEmployeeKeys.has(k))) : projectEmployeeKeys)
|
|
3044
|
+
: new Set(areaPersonaOrder.keys());
|
|
2981
3045
|
|
|
2982
3046
|
for (const key of filteredEmployeeKeys) {
|
|
2983
3047
|
if (doneGroups.has(key)) continue;
|
|
2984
|
-
const p =
|
|
3048
|
+
const p = getConversationPersona({ personaKey: key });
|
|
2985
3049
|
doneGroups.set(key, {
|
|
2986
3050
|
key,
|
|
2987
3051
|
label: p ? p.displayName : key,
|
|
@@ -2993,7 +3057,7 @@ function renderRail() {
|
|
|
2993
3057
|
|
|
2994
3058
|
// Compute latest kickoff timestamp per employee for employee tree sorting.
|
|
2995
3059
|
const employeeLatest = new Map();
|
|
2996
|
-
for (const conv of
|
|
3060
|
+
for (const conv of scopedConversations) {
|
|
2997
3061
|
if (!conv) continue;
|
|
2998
3062
|
const key = conv.personaKey || conversationAgentName(conv);
|
|
2999
3063
|
if (!key) continue;
|
|
@@ -3004,6 +3068,7 @@ function renderRail() {
|
|
|
3004
3068
|
// Sort employee groups: employees with completed runs first (latest kickoff),
|
|
3005
3069
|
// then no-run employees last (stable by key).
|
|
3006
3070
|
const sortedDoneGroups = [...doneGroups.values()].sort((a, b) => {
|
|
3071
|
+
if (!isProject) return (areaPersonaOrder.get(a.key) ?? Infinity) - (areaPersonaOrder.get(b.key) ?? Infinity);
|
|
3007
3072
|
const aLatest = employeeLatest.get(a.key) || 0;
|
|
3008
3073
|
const bLatest = employeeLatest.get(b.key) || 0;
|
|
3009
3074
|
const aHas = aLatest > 0 ? 1 : 0;
|
|
@@ -3028,7 +3093,7 @@ function renderRail() {
|
|
|
3028
3093
|
emptyWrap.appendChild(em);
|
|
3029
3094
|
inProgressSec.appendChild(emptyWrap);
|
|
3030
3095
|
}
|
|
3031
|
-
|
|
3096
|
+
host.appendChild(inProgressSec);
|
|
3032
3097
|
|
|
3033
3098
|
// ----- SECTION 2: Per-employee accordion tree -----
|
|
3034
3099
|
// Each employee gets their own accordion showing their COMPLETED runs underneath,
|
|
@@ -3076,11 +3141,11 @@ function renderRail() {
|
|
|
3076
3141
|
// personaKey is passed separately so renderCpRows can look up the jobIds correctly.
|
|
3077
3142
|
if (group.key.startsWith('custom:')) {
|
|
3078
3143
|
const displaySlug = group.label ? group.label.toLowerCase().replace(/\s+/g, '-') : group.key.replace('custom:', '');
|
|
3079
|
-
focusComposerForDelegate({ prefixSearch: '/' + displaySlug, personaKey: group.key });
|
|
3144
|
+
focusComposerForDelegate({ prefixSearch: '/' + displaySlug, personaKey: group.key, actionContext: managementActionContext(area) });
|
|
3080
3145
|
return;
|
|
3081
3146
|
}
|
|
3082
3147
|
const personaKey = group.sample && group.sample.personaKey ? group.key : null;
|
|
3083
|
-
focusComposerForDelegate(personaKey ? { employeeId: state.bootstrap && state.bootstrap.preferences ? state.bootstrap.preferences.employeeId : 'claude', prefixSearch: '/' + personaKey } : {});
|
|
3148
|
+
focusComposerForDelegate(personaKey ? { employeeId: state.bootstrap && state.bootstrap.preferences ? state.bootstrap.preferences.employeeId : 'claude', prefixSearch: '/' + personaKey, actionContext: managementActionContext(area) } : { actionContext: managementActionContext(area) });
|
|
3084
3149
|
});
|
|
3085
3150
|
}
|
|
3086
3151
|
let manageBtn = null;
|
|
@@ -3138,7 +3203,7 @@ function renderRail() {
|
|
|
3138
3203
|
emptyWrap.appendChild(em);
|
|
3139
3204
|
details.appendChild(emptyWrap);
|
|
3140
3205
|
}
|
|
3141
|
-
appendEmployeeGroupWithActions(
|
|
3206
|
+
appendEmployeeGroupWithActions(host, details);
|
|
3142
3207
|
}
|
|
3143
3208
|
|
|
3144
3209
|
// No FRAIM employee group: completed runs with no owning persona.
|
|
@@ -3165,29 +3230,29 @@ function renderRail() {
|
|
|
3165
3230
|
wcSummary.appendChild(wcCount);
|
|
3166
3231
|
wcDetails.appendChild(wcSummary);
|
|
3167
3232
|
wcDetails.appendChild(buildAreaGroupList(noFraimEmployeeConvs, {}, childrenByManager));
|
|
3168
|
-
|
|
3233
|
+
host.appendChild(wcDetails);
|
|
3169
3234
|
}
|
|
3170
3235
|
|
|
3171
3236
|
// #769: hire-pending polling indicator
|
|
3172
|
-
if (typeof tf !== 'undefined' && tf && tf.hirePendingState === 'polling') {
|
|
3237
|
+
if (isProject && typeof tf !== 'undefined' && tf && tf.hirePendingState === 'polling') {
|
|
3173
3238
|
const pendingRow = document.createElement('div');
|
|
3174
3239
|
pendingRow.className = 'hire-pending-row';
|
|
3175
3240
|
pendingRow.id = 'hire-pending-row';
|
|
3176
3241
|
pendingRow.innerHTML = '<div class="hire-pending-spinner"></div><span class="hire-pending-text">Checking your new hire…</span>';
|
|
3177
|
-
|
|
3178
|
-
} else if (typeof tf !== 'undefined' && tf && tf.hirePendingState === 'timeout') {
|
|
3242
|
+
host.appendChild(pendingRow);
|
|
3243
|
+
} else if (isProject && typeof tf !== 'undefined' && tf && tf.hirePendingState === 'timeout') {
|
|
3179
3244
|
const timeoutRow = document.createElement('div');
|
|
3180
3245
|
timeoutRow.className = 'hire-timeout-row';
|
|
3181
3246
|
timeoutRow.id = 'hire-timeout-row';
|
|
3182
3247
|
timeoutRow.innerHTML = '<span class="hire-timeout-icon">⚠</span><span class="hire-timeout-text">Taking longer than expected. </span><a href="javascript:window.location.reload()">Reload to check</a>';
|
|
3183
|
-
|
|
3248
|
+
host.appendChild(timeoutRow);
|
|
3184
3249
|
}
|
|
3185
3250
|
|
|
3186
3251
|
// #851 Option A: tree always has content (at minimum In Progress + employee tree).
|
|
3187
|
-
const runsSection =
|
|
3252
|
+
const runsSection = host.closest('.rail-section--runs');
|
|
3188
3253
|
if (runsSection) runsSection.hidden = false;
|
|
3189
3254
|
// #824: re-attach an open run-delete confirm that this rebuild would otherwise drop.
|
|
3190
|
-
tfReopenPendingRunDelete(
|
|
3255
|
+
tfReopenPendingRunDelete(host);
|
|
3191
3256
|
}
|
|
3192
3257
|
|
|
3193
3258
|
function statusLabel(s) {
|
|
@@ -3726,9 +3791,9 @@ function showHireStrip(job, conversationId, instructions, employeeId) {
|
|
|
3726
3791
|
state._hireStripPending = { job, conversationId, instructions, employeeId };
|
|
3727
3792
|
els['hire-strip'].hidden = false;
|
|
3728
3793
|
// Switch workspace to conv mode so the hire strip (inside .page) is visible
|
|
3729
|
-
// even when
|
|
3794
|
+
// even when the manager was reading Project Info.
|
|
3730
3795
|
const wc = document.querySelector('#proj-workspace .workspace-conv');
|
|
3731
|
-
if (wc)
|
|
3796
|
+
if (wc) tfToggleAreaView('projects', 'delegate');
|
|
3732
3797
|
}
|
|
3733
3798
|
|
|
3734
3799
|
function hideHireStrip() {
|
|
@@ -3864,20 +3929,8 @@ function syncMicroArchive(conv) {
|
|
|
3864
3929
|
// coach, and micro-manage all have room. Toggle the mode class the CSS keys off,
|
|
3865
3930
|
// and reflect the state in the sidebar's Brief nav entry.
|
|
3866
3931
|
function tfApplyWorkspaceMode() {
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
const hasConv = typeof activeConversation === 'function' && !!activeConversation();
|
|
3870
|
-
// Issue #1657 Treatment 3: "no active conversation" no longer implies "show
|
|
3871
|
-
// the Brief page" by itself - the persistent composer's idle state (#empty)
|
|
3872
|
-
// must stay visible then too (R1: present at all times). state.showProjectInfo
|
|
3873
|
-
// is the explicit signal only the "Project Info" nav entry sets; any real
|
|
3874
|
-
// active conversation still wins over it, so the many state.activeId call
|
|
3875
|
-
// sites elsewhere don't each need to clear this flag themselves.
|
|
3876
|
-
const showBrief = !!state.showProjectInfo && !hasConv;
|
|
3877
|
-
wc.classList.toggle('mode-conv', !showBrief);
|
|
3878
|
-
wc.classList.toggle('mode-brief', showBrief);
|
|
3879
|
-
const briefNav = document.getElementById('tree-brief-nav');
|
|
3880
|
-
if (briefNav) briefNav.classList.toggle('active', showBrief);
|
|
3932
|
+
if (typeof tf === 'undefined') return;
|
|
3933
|
+
tfApplyAreaView('projects', tfResolveAreaView('projects', activeConversation()));
|
|
3881
3934
|
}
|
|
3882
3935
|
|
|
3883
3936
|
function formatMicroAuditKind(event) {
|
|
@@ -6181,6 +6234,7 @@ function resizeComposerTextarea() {
|
|
|
6181
6234
|
}
|
|
6182
6235
|
|
|
6183
6236
|
function syncSendButton() {
|
|
6237
|
+
syncVoiceInput();
|
|
6184
6238
|
syncReviewHint();
|
|
6185
6239
|
resizeComposerTextarea();
|
|
6186
6240
|
const conv = activeConversation();
|
|
@@ -6293,6 +6347,7 @@ function syncManagedDelegationAccess(conv = activeConversation()) {
|
|
|
6293
6347
|
}
|
|
6294
6348
|
}
|
|
6295
6349
|
if (managed) closeAllCoachDropdowns();
|
|
6350
|
+
syncVoiceInput();
|
|
6296
6351
|
syncComposerPlaceholder(conv);
|
|
6297
6352
|
syncComposerStopButton(conv);
|
|
6298
6353
|
}
|
|
@@ -7981,8 +8036,8 @@ function parseCpSearch(text) {
|
|
|
7981
8036
|
function focusComposerForDelegate(opts) {
|
|
7982
8037
|
state.cpActionContext = (opts && opts.actionContext) || managementActionContext(state.pendingNextJobArea || undefined);
|
|
7983
8038
|
state.activeId = null;
|
|
7984
|
-
persistActiveConversationSelection(null, { projectPath: state.projectPath || '' });
|
|
7985
|
-
|
|
8039
|
+
persistActiveConversationSelection(null, tf.area === 'projects' ? { projectPath: state.projectPath || '' } : { scope: tf.area });
|
|
8040
|
+
tf.areaSelection[tfAreaSelectionKey(tf.area)] = 'delegate';
|
|
7986
8041
|
if (tf.area === 'company' || tf.area === 'manager') {
|
|
7987
8042
|
tf.areaSelection[tf.area] = 'delegate';
|
|
7988
8043
|
if (tf.area === 'company') tfRenderCompany();
|
|
@@ -8942,7 +8997,7 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
8942
8997
|
};
|
|
8943
8998
|
upsertConversation(conv);
|
|
8944
8999
|
state.activeId = conv.id;
|
|
8945
|
-
|
|
9000
|
+
tf.areaSelection[tfAreaSelectionKey(context.area)] = conv.id;
|
|
8946
9001
|
persistActiveConversationSelection(conv.id);
|
|
8947
9002
|
// Issue #539: make Cmd/Ctrl+Shift+R available as soon as the run is queued,
|
|
8948
9003
|
// not only after the server responds with the run id.
|
|
@@ -9455,23 +9510,13 @@ function switchToConversation(id) {
|
|
|
9455
9510
|
tfShowArea(targetArea);
|
|
9456
9511
|
}
|
|
9457
9512
|
state.activeId = id;
|
|
9513
|
+
tf.areaSelection[tfAreaSelectionKey(targetArea)] = id;
|
|
9458
9514
|
persistActiveConversationSelection(id);
|
|
9459
9515
|
// Issue #1065: a find session belongs to one thread. srchOpenResult re-opens it with the search
|
|
9460
9516
|
// query immediately after this call, which is what lands the manager on the passage (R45).
|
|
9461
9517
|
if (typeof convFindClose === 'function') convFindClose();
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
if (targetArea === 'company' || targetArea === 'manager') {
|
|
9465
|
-
tfEnsurePageInArea(targetArea);
|
|
9466
|
-
const host = document.getElementById(targetArea + '-conv-host');
|
|
9467
|
-
const info = document.getElementById(targetArea + '-info-view');
|
|
9468
|
-
if (host) host.hidden = false;
|
|
9469
|
-
if (info) info.hidden = true;
|
|
9470
|
-
// #1397: an area selection represents every explicit destination, not only
|
|
9471
|
-
// the Info view. Poll-driven renders must preserve this conversation until
|
|
9472
|
-
// the user chooses another destination or the conversation disappears.
|
|
9473
|
-
if (tf.areaSelection) tf.areaSelection[targetArea] = id;
|
|
9474
|
-
}
|
|
9518
|
+
tfEnsurePageInArea(targetArea);
|
|
9519
|
+
tfApplyAreaView(targetArea, tfResolveAreaView(targetArea, conv));
|
|
9475
9520
|
renderRail();
|
|
9476
9521
|
renderActive();
|
|
9477
9522
|
hydrateConversationBody(conv ? convBucketKey(conv) : state.projectPath, id).catch((error) =>
|
|
@@ -10156,7 +10201,7 @@ function wireEvents() {
|
|
|
10156
10201
|
// just defocuses the current conversation and focuses the always-present
|
|
10157
10202
|
// composer, the same way clicking an employee starts/focuses a conversation.
|
|
10158
10203
|
els['new-conv-btn'].addEventListener('click', () => {
|
|
10159
|
-
|
|
10204
|
+
tf.areaSelection[tfAreaSelectionKey(tf.area)] = 'delegate';
|
|
10160
10205
|
// renderActive() re-applies the workspace mode unconditionally (needed
|
|
10161
10206
|
// when coming from the Project Info page, since activeId is already
|
|
10162
10207
|
// null there too and focusComposerForDelegate only re-renders when
|
|
@@ -10292,6 +10337,7 @@ function wireEvents() {
|
|
|
10292
10337
|
}
|
|
10293
10338
|
});
|
|
10294
10339
|
}
|
|
10340
|
+
els['voice-input-btn'].addEventListener('click', startVoiceInput);
|
|
10295
10341
|
els['coach-text'].addEventListener('input', () => {
|
|
10296
10342
|
syncSendButton();
|
|
10297
10343
|
// Issue #1657 R2: drive the inline suggestion list while idle (no
|
|
@@ -11012,10 +11058,7 @@ const SRCH_MIN_QUERY_LENGTH = 2;
|
|
|
11012
11058
|
const SRCH_DEBOUNCE_MS = 150;
|
|
11013
11059
|
// Every element search hides to show results. Their pre-search hidden flags are captured verbatim
|
|
11014
11060
|
// so clearing restores the exact surface state, whatever it was (R6).
|
|
11015
|
-
const SRCH_HIDEABLE_IDS = [
|
|
11016
|
-
'hub-conv-page', 'proj-overview-page', 'proj-kanban',
|
|
11017
|
-
'manager-info-view', 'manager-conv-host', 'company-info-view', 'company-conv-host',
|
|
11018
|
-
];
|
|
11061
|
+
const SRCH_HIDEABLE_IDS = ['project', 'all', 'manager', 'company'].flatMap(srchHiddenForScope);
|
|
11019
11062
|
const SRCH_PANEL_IDS = ['project-results', 'overview-results', 'manager-results', 'company-results'];
|
|
11020
11063
|
|
|
11021
11064
|
const srch = {
|
|
@@ -11053,7 +11096,7 @@ function srchHiddenForScope(scope) {
|
|
|
11053
11096
|
if (scope === 'company') return ['company-info-view', 'company-conv-host'];
|
|
11054
11097
|
if (scope === 'manager') return ['manager-info-view', 'manager-conv-host'];
|
|
11055
11098
|
if (scope === 'all') return ['proj-overview-page', 'proj-kanban'];
|
|
11056
|
-
return ['hub-conv-page'];
|
|
11099
|
+
return ['proj-context-top', 'hub-conv-page'];
|
|
11057
11100
|
}
|
|
11058
11101
|
|
|
11059
11102
|
function srchSyncChip() {
|
|
@@ -11134,6 +11177,10 @@ function srchHidePanel() {
|
|
|
11134
11177
|
srch.surface = null;
|
|
11135
11178
|
}
|
|
11136
11179
|
srch.active = false;
|
|
11180
|
+
// Scope switches may happen while searching. Resolve the current destination
|
|
11181
|
+
// instead of letting an old snapshot override the current scope's selection.
|
|
11182
|
+
if (tf.area === 'projects') tfApplyWorkspaceMode();
|
|
11183
|
+
else if (tf.area === 'manager' || tf.area === 'company') tfRenderScopeView(tf.area);
|
|
11137
11184
|
}
|
|
11138
11185
|
|
|
11139
11186
|
function srchClear() {
|
|
@@ -12255,75 +12302,83 @@ async function tfSelectProjectView(view, projectId) {
|
|
|
12255
12302
|
}
|
|
12256
12303
|
|
|
12257
12304
|
function tfStoredTreeWidth() {
|
|
12258
|
-
|
|
12305
|
+
let raw;
|
|
12306
|
+
try { raw = window.localStorage.getItem(STORAGE_KEY_TREE_WIDTH); }
|
|
12307
|
+
catch { return state.treeNavWidth || TREE_WIDTH_DEFAULT; }
|
|
12259
12308
|
const parsed = Number(raw);
|
|
12260
|
-
if (!Number.isFinite(parsed)) return TREE_WIDTH_DEFAULT;
|
|
12309
|
+
if (raw === null || raw.trim() === '' || !Number.isFinite(parsed)) return TREE_WIDTH_DEFAULT;
|
|
12261
12310
|
return Math.min(TREE_WIDTH_MAX, Math.max(TREE_WIDTH_MIN, Math.round(parsed)));
|
|
12262
12311
|
}
|
|
12263
12312
|
|
|
12264
12313
|
function tfApplyTreeNavState() {
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12314
|
+
let collapsed = !!state.treeNavCollapsed;
|
|
12315
|
+
try { collapsed = window.localStorage.getItem(STORAGE_KEY_TREE_COLLAPSED) === '1'; } catch { /* Session state remains usable without storage. */ }
|
|
12316
|
+
document.querySelectorAll('.scope-shell').forEach((workspace) => {
|
|
12317
|
+
workspace.style.setProperty('--tree-sidebar-width', tfStoredTreeWidth() + 'px');
|
|
12318
|
+
workspace.classList.toggle('tree-collapsed', collapsed);
|
|
12319
|
+
const button = workspace.querySelector('.tree-collapse-btn');
|
|
12320
|
+
if (button) {
|
|
12321
|
+
const label = collapsed ? 'Expand navigation' : 'Collapse navigation';
|
|
12322
|
+
button.setAttribute('aria-label', label);
|
|
12323
|
+
button.setAttribute('aria-expanded', String(!collapsed));
|
|
12324
|
+
button.title = label;
|
|
12325
|
+
}
|
|
12326
|
+
});
|
|
12276
12327
|
}
|
|
12277
12328
|
|
|
12278
12329
|
function tfSetTreeCollapsed(collapsed) {
|
|
12279
|
-
|
|
12280
|
-
|
|
12330
|
+
state.treeNavCollapsed = collapsed;
|
|
12331
|
+
try {
|
|
12332
|
+
if (collapsed) window.localStorage.setItem(STORAGE_KEY_TREE_COLLAPSED, '1');
|
|
12333
|
+
else window.localStorage.removeItem(STORAGE_KEY_TREE_COLLAPSED);
|
|
12334
|
+
} catch { /* Keep the preference for this session when storage is blocked. */ }
|
|
12281
12335
|
tfApplyTreeNavState();
|
|
12282
12336
|
}
|
|
12283
12337
|
|
|
12284
12338
|
function tfWireTreeNavControls() {
|
|
12285
|
-
const workspace = document.getElementById('proj-workspace');
|
|
12286
|
-
const collapseBtn = document.getElementById('tree-collapse-btn');
|
|
12287
|
-
const resizer = document.getElementById('tree-resizer');
|
|
12288
|
-
if (!workspace) return;
|
|
12289
12339
|
tfApplyTreeNavState();
|
|
12290
|
-
|
|
12291
|
-
collapseBtn
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
12296
|
-
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
dragging = false;
|
|
12304
|
-
|
|
12305
|
-
|
|
12306
|
-
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12340
|
+
document.querySelectorAll('.scope-shell').forEach((workspace) => {
|
|
12341
|
+
const collapseBtn = workspace.querySelector('.tree-collapse-btn');
|
|
12342
|
+
const resizer = workspace.querySelector('.tree-resizer');
|
|
12343
|
+
if (collapseBtn && !collapseBtn.dataset.wired) {
|
|
12344
|
+
collapseBtn.dataset.wired = '1';
|
|
12345
|
+
collapseBtn.addEventListener('click', () => {
|
|
12346
|
+
tfSetTreeCollapsed(!workspace.classList.contains('tree-collapsed'));
|
|
12347
|
+
});
|
|
12348
|
+
}
|
|
12349
|
+
if (resizer && !resizer.dataset.wired) {
|
|
12350
|
+
resizer.dataset.wired = '1';
|
|
12351
|
+
let startX = 0;
|
|
12352
|
+
let startWidth = TREE_WIDTH_DEFAULT;
|
|
12353
|
+
let dragging = false;
|
|
12354
|
+
const stopDrag = () => {
|
|
12355
|
+
if (!dragging) return;
|
|
12356
|
+
dragging = false;
|
|
12357
|
+
workspace.classList.remove('tree-resizing');
|
|
12358
|
+
document.removeEventListener('pointermove', onMove);
|
|
12359
|
+
document.removeEventListener('pointerup', stopDrag);
|
|
12360
|
+
document.removeEventListener('pointercancel', stopDrag);
|
|
12361
|
+
};
|
|
12362
|
+
const onMove = (event) => {
|
|
12363
|
+
if (!dragging) return;
|
|
12364
|
+
const nextWidth = Math.min(TREE_WIDTH_MAX, Math.max(TREE_WIDTH_MIN, startWidth + event.clientX - startX));
|
|
12365
|
+
state.treeNavWidth = Math.round(nextWidth);
|
|
12366
|
+
try { window.localStorage.setItem(STORAGE_KEY_TREE_WIDTH, String(state.treeNavWidth)); } catch { /* Session fallback. */ }
|
|
12367
|
+
tfApplyTreeNavState();
|
|
12368
|
+
};
|
|
12369
|
+
resizer.addEventListener('pointerdown', (event) => {
|
|
12370
|
+
event.preventDefault();
|
|
12371
|
+
tfSetTreeCollapsed(false);
|
|
12372
|
+
dragging = true;
|
|
12373
|
+
startX = event.clientX;
|
|
12374
|
+
startWidth = tfStoredTreeWidth();
|
|
12375
|
+
workspace.classList.add('tree-resizing');
|
|
12376
|
+
document.addEventListener('pointermove', onMove);
|
|
12377
|
+
document.addEventListener('pointerup', stopDrag);
|
|
12378
|
+
document.addEventListener('pointercancel', stopDrag);
|
|
12379
|
+
});
|
|
12380
|
+
}
|
|
12381
|
+
});
|
|
12327
12382
|
}
|
|
12328
12383
|
|
|
12329
12384
|
// ---------------------------------------------------------------------------
|
|
@@ -12880,88 +12935,46 @@ function tfTreeSep() {
|
|
|
12880
12935
|
return sep;
|
|
12881
12936
|
}
|
|
12882
12937
|
function tfRenderTree() {
|
|
12883
|
-
|
|
12938
|
+
renderScopeNavigation(document.getElementById('tree-sidebar'), 'projects');
|
|
12939
|
+
}
|
|
12940
|
+
|
|
12941
|
+
function renderScopeNavigation(host, area) {
|
|
12884
12942
|
if (!host) return;
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
host.
|
|
12890
|
-
// #521: Project Info is the project's own page (brief + learnings, not stacked
|
|
12891
|
-
// over every conversation). This entry deselects any active run, shows that page.
|
|
12943
|
+
const isProject = area === 'projects';
|
|
12944
|
+
const oldSection = isProject
|
|
12945
|
+
? els['conv-list'].closest('.rail-section--runs')
|
|
12946
|
+
: host.querySelector('.rail-section--runs');
|
|
12947
|
+
host.replaceChildren();
|
|
12892
12948
|
const briefNav = document.createElement('button');
|
|
12893
|
-
briefNav.id = 'tree-brief-nav';
|
|
12949
|
+
if (isProject) briefNav.id = 'tree-brief-nav';
|
|
12894
12950
|
briefNav.type = 'button';
|
|
12895
12951
|
briefNav.className = 'tree-brief-nav';
|
|
12896
|
-
briefNav.
|
|
12897
|
-
briefNav.addEventListener('click', () =>
|
|
12898
|
-
state.activeId = null;
|
|
12899
|
-
state.showProjectInfo = true; // Issue #1657: the one explicit trigger for the Brief page.
|
|
12900
|
-
// #1565: was persistConversations() (debounced full-project PUT - reads/rewrites every
|
|
12901
|
-
// conversation, freezing Electron's main process on large histories). Clearing the active
|
|
12902
|
-
// selection is a body-safe activeId-only PATCH, same narrow path every other activeId
|
|
12903
|
-
// change already uses.
|
|
12904
|
-
if (typeof persistActiveConversationSelection === 'function') {
|
|
12905
|
-
persistActiveConversationSelection(null, { projectPath: state.projectPath || '' });
|
|
12906
|
-
}
|
|
12907
|
-
if (typeof renderRail === 'function') renderRail();
|
|
12908
|
-
renderActive();
|
|
12909
|
-
tfRenderProjectContextTop();
|
|
12910
|
-
tfApplyWorkspaceMode();
|
|
12911
|
-
});
|
|
12952
|
+
briefNav.textContent = '📋 ' + (isProject ? 'Project' : area === 'company' ? 'Company' : 'Manager') + ' Info';
|
|
12953
|
+
briefNav.addEventListener('click', () => tfToggleAreaView(area, 'info'));
|
|
12912
12954
|
host.appendChild(briefNav);
|
|
12913
12955
|
host.appendChild(tfTreeSep());
|
|
12914
|
-
|
|
12915
|
-
// lifecycle actions now live where their output lands — Run Project Onboarding
|
|
12916
|
-
// in the Brief section and Sleep on learnings in the Learnings section
|
|
12917
|
-
// (see tfRenderProjectContextTop). No separate tree section.
|
|
12918
|
-
// The legacy standalone "+ New job" entry (#new-conv-btn → openModal → the
|
|
12919
|
-
// #job-catalog/#next1/#instructions/#start chain) lives in the old `.rail`,
|
|
12920
|
-
// which the shell suppresses (display:none). Relocate the live node to the
|
|
12921
|
-
// top of the visible tree so the standalone conversation flow that the
|
|
12922
|
-
// shipped #339/#347/#429 Hub tests depend on stays clickable. Moving the
|
|
12923
|
-
// node preserves its cached `els` reference and wired click listener.
|
|
12924
|
-
appendManagementActions(host, 'projects');
|
|
12925
|
-
host.appendChild(tfTreeSep());
|
|
12926
|
-
// #693 R1 (PR round 2): the per-employee "Runs" accordions (renderRail's
|
|
12927
|
-
// conv-employee-group) ARE the single employees section — they list every
|
|
12928
|
-
// project employee with that employee's runs nested under their accordion.
|
|
12929
|
-
// Relocate that section into the tree; the redundant emp-hero list is retired.
|
|
12930
|
-
const convList = (typeof els !== 'undefined' && els['conv-list']) || document.getElementById('conv-list');
|
|
12931
|
-
const runsSection = convList && convList.closest('.rail-section--runs');
|
|
12932
|
-
if (runsSection) {
|
|
12933
|
-
runsSection.classList.add('tree-runs');
|
|
12934
|
-
host.appendChild(runsSection);
|
|
12935
|
-
}
|
|
12936
|
-
// #693 R1 (PR round 2): the redundant emp-hero employee list was removed. The
|
|
12937
|
-
// relocated Runs section above (renderRail's conv-employee-group) is now the
|
|
12938
|
-
// single employees section — every project employee, with their runs nested
|
|
12939
|
-
// under their own accordion, plus per-employee assign/hire-manager.
|
|
12940
|
-
|
|
12941
|
-
// #521 R5/R6.4: the project brief and learnings moved OUT of the tree into the
|
|
12942
|
-
// collapsible top sections (#proj-brief-acc / #proj-learnings-acc), rendered by
|
|
12943
|
-
// tfRenderProjectContextTop(). The tree is now employees + jobs only.
|
|
12956
|
+
appendManagementActions(host, area);
|
|
12944
12957
|
host.appendChild(tfTreeSep());
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
|
|
12958
|
+
const section = oldSection || document.createElement('section');
|
|
12959
|
+
section.className = 'rail-section rail-section--runs tree-runs';
|
|
12960
|
+
if (!oldSection) {
|
|
12961
|
+
const title = document.createElement('div');
|
|
12962
|
+
title.className = 'rail-section-label';
|
|
12963
|
+
title.textContent = 'Employees';
|
|
12964
|
+
section.appendChild(title);
|
|
12965
|
+
const list = document.createElement('div');
|
|
12966
|
+
list.className = 'conv-list';
|
|
12967
|
+
section.appendChild(list);
|
|
12968
|
+
}
|
|
12969
|
+
host.appendChild(section);
|
|
12970
|
+
if (!isProject) renderScopeEmployees(section.querySelector('.conv-list'), area);
|
|
12971
|
+
if (!isProject) {
|
|
12972
|
+
const note = document.createElement('div');
|
|
12973
|
+
note.className = 'area-rail-note';
|
|
12974
|
+
note.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.";
|
|
12975
|
+
host.appendChild(note);
|
|
12976
|
+
}
|
|
12963
12977
|
host.appendChild(tfTreeSep());
|
|
12964
|
-
|
|
12965
12978
|
// Dot legend.
|
|
12966
12979
|
const legend = document.createElement('div');
|
|
12967
12980
|
legend.className = 'tree-legend';
|
|
@@ -12986,10 +12999,9 @@ function tfRenderTree() {
|
|
|
12986
12999
|
legend.appendChild(row);
|
|
12987
13000
|
}
|
|
12988
13001
|
host.appendChild(legend);
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
});
|
|
13002
|
+
const fallback = isProject ? activeConversation() : tfActiveScopedConv(area);
|
|
13003
|
+
briefNav.classList.toggle('active', tfResolveAreaView(area, fallback).info);
|
|
13004
|
+
tfReopenPendingRunDelete(host);
|
|
12993
13005
|
}
|
|
12994
13006
|
|
|
12995
13007
|
function tfSelectAssignmentConversation(assignment) {
|
|
@@ -13020,6 +13032,9 @@ function tfProjectOnboardingRun() {
|
|
|
13020
13032
|
}
|
|
13021
13033
|
|
|
13022
13034
|
function tfRenderProjectContextTop() {
|
|
13035
|
+
const heading = document.getElementById('project-info-heading');
|
|
13036
|
+
const project = tf.projects.find((entry) => entry.id === tf.activeProjectId);
|
|
13037
|
+
if (heading) heading.textContent = project ? project.name : ((state.projectPath || '').split(/[\\/]/).filter(Boolean).pop() || 'Project');
|
|
13023
13038
|
// Issue #1198: which folder this project points at, always on screen.
|
|
13024
13039
|
tfRenderProjectFolderRow();
|
|
13025
13040
|
// Onboarding loop framing renders ABOVE the accordions (#proj-onboarding-banner)
|
|
@@ -14341,16 +14356,51 @@ function tfActiveMgrConv() {
|
|
|
14341
14356
|
// conversations behind that destination, but it may not choose a different one.
|
|
14342
14357
|
// A deleted or re-scoped conversation invalidates the selection and restores the
|
|
14343
14358
|
// area's normal auto-home behavior.
|
|
14359
|
+
function tfAreaSelectionKey(area) {
|
|
14360
|
+
return area === 'projects' ? 'project:' + (tf.activeProjectId || state.projectPath || 'current') : area;
|
|
14361
|
+
}
|
|
14362
|
+
|
|
14344
14363
|
function tfResolveAreaView(area, fallbackConv) {
|
|
14345
|
-
const
|
|
14364
|
+
const key = tfAreaSelectionKey(area);
|
|
14365
|
+
const selected = tf.areaSelection[key];
|
|
14346
14366
|
if (selected === 'info') return { info: true, conversation: null };
|
|
14347
14367
|
if (selected === 'delegate') return { info: false, conversation: null, delegate: true };
|
|
14348
14368
|
if (selected) {
|
|
14349
14369
|
const conv = findConversation(selected);
|
|
14350
|
-
|
|
14351
|
-
|
|
14370
|
+
const scope = area === 'projects' ? 'project' : area;
|
|
14371
|
+
const belongs = conv && convScope(conv) === scope && (area !== 'projects' || projectConversations().includes(conv));
|
|
14372
|
+
if (belongs) return { info: false, conversation: conv };
|
|
14373
|
+
tf.areaSelection[key] = null;
|
|
14374
|
+
}
|
|
14375
|
+
const conv = fallbackConv && convScope(fallbackConv) === (area === 'projects' ? 'project' : area) ? fallbackConv : null;
|
|
14376
|
+
return { info: area !== 'projects' && !conv, conversation: conv, delegate: area === 'projects' && !conv };
|
|
14377
|
+
}
|
|
14378
|
+
|
|
14379
|
+
// Info and runs use hidden on every scope; search is an overlay on that selection.
|
|
14380
|
+
function tfApplyAreaView(area, view) {
|
|
14381
|
+
const isProject = area === 'projects';
|
|
14382
|
+
const info = document.getElementById(isProject ? 'proj-context-top' : area + '-info-view');
|
|
14383
|
+
const host = document.getElementById(isProject ? 'hub-conv-page' : area + '-conv-host');
|
|
14384
|
+
const searching = typeof srch !== 'undefined' && srch.active;
|
|
14385
|
+
if (info) info.hidden = searching || !view.info;
|
|
14386
|
+
if (host && (!isProject || host.closest('#proj-workspace'))) host.hidden = searching || view.info;
|
|
14387
|
+
const page = document.getElementById('hub-conv-page');
|
|
14388
|
+
if (!isProject && host && page && host.contains(page)) page.hidden = searching || view.info;
|
|
14389
|
+
const rail = document.getElementById(isProject ? 'tree-sidebar' : area + '-rail');
|
|
14390
|
+
const button = rail && rail.querySelector('.tree-brief-nav');
|
|
14391
|
+
if (button) button.classList.toggle('active', view.info);
|
|
14392
|
+
}
|
|
14393
|
+
|
|
14394
|
+
function tfRenderScopeView(area) {
|
|
14395
|
+
const view = tfResolveAreaView(area, tfActiveScopedConv(area));
|
|
14396
|
+
if (tf.area === area && !view.info) {
|
|
14397
|
+
state.activeId = view.conversation ? view.conversation.id : null;
|
|
14398
|
+
tfEnsurePageInArea(area);
|
|
14399
|
+
renderActive();
|
|
14400
|
+
} else if (tf.area === area) {
|
|
14401
|
+
tfEnsurePageInArea('projects');
|
|
14352
14402
|
}
|
|
14353
|
-
|
|
14403
|
+
tfApplyAreaView(area, view);
|
|
14354
14404
|
}
|
|
14355
14405
|
|
|
14356
14406
|
function tfAreaScopedConversations(area) {
|
|
@@ -14395,138 +14445,6 @@ function tfStartManagerPersonaJob(jobId) {
|
|
|
14395
14445
|
}
|
|
14396
14446
|
|
|
14397
14447
|
|
|
14398
|
-
// #1416: capture each employee group's open/closed state from the area rail (Manager or
|
|
14399
|
-
// Company) before it is rebuilt, mirroring renderRail()'s preservedEmployeeGroupOpen() for
|
|
14400
|
-
// the Projects rail. Without this, tfRenderManager()/tfRenderCompany()'s ~1-1.5s poll-driven
|
|
14401
|
-
// rebuild wiped the DOM and re-created every group, so a manually collapsed group snapped
|
|
14402
|
-
// back open on the next tick and there was no way to tell a no-run group from one the user
|
|
14403
|
-
// deliberately expanded.
|
|
14404
|
-
function captureAreaEmployeeGroupOpenState(rail) {
|
|
14405
|
-
const openState = new Map();
|
|
14406
|
-
if (!rail) return openState;
|
|
14407
|
-
rail.querySelectorAll('details.conv-employee-group[data-employee-key]').forEach((details) => {
|
|
14408
|
-
const key = details.dataset ? details.dataset.employeeKey : '';
|
|
14409
|
-
if (key) openState.set(key, details.open);
|
|
14410
|
-
});
|
|
14411
|
-
return openState;
|
|
14412
|
-
}
|
|
14413
|
-
|
|
14414
|
-
// #702: render a hired persona as an employee group on the Manager rail — the SAME
|
|
14415
|
-
// visual as the Projects rail employee groups (avatar + name + role + count + "+" that
|
|
14416
|
-
// opens the job palette), with the persona's MANAGER-invoked runs listed under it.
|
|
14417
|
-
// Keeps a persona looking like an employee, consistent across tabs.
|
|
14418
|
-
function tfRenderAreaPersonaGroup(rail, persona, area, employeeGroupOpenState) {
|
|
14419
|
-
const sample = { personaKey: persona.key };
|
|
14420
|
-
const runs = tfAreaScopedConversations(area).filter(
|
|
14421
|
-
(c) => c.personaKey === persona.key && conversationUiState(c) === 'complete'
|
|
14422
|
-
);
|
|
14423
|
-
const details = document.createElement('details');
|
|
14424
|
-
details.className = 'conv-employee-group';
|
|
14425
|
-
details.dataset.employeeKey = persona.key;
|
|
14426
|
-
// Active runs now live in the "In Progress" section above; this accordion shows only
|
|
14427
|
-
// completed runs so it defaults closed (user expands when they want history).
|
|
14428
|
-
const defaultOpen = false;
|
|
14429
|
-
details.open = employeeGroupOpenState && employeeGroupOpenState.has(persona.key)
|
|
14430
|
-
? employeeGroupOpenState.get(persona.key)
|
|
14431
|
-
: defaultOpen;
|
|
14432
|
-
const summary = document.createElement('summary');
|
|
14433
|
-
summary.className = 'conv-employee-tab';
|
|
14434
|
-
const avatar = buildConversationEmployeeAvatar(sample, 'conv-employee-avatar');
|
|
14435
|
-
const copy = document.createElement('span'); copy.className = 'conv-employee-tab-copy';
|
|
14436
|
-
const label = document.createElement('strong'); label.className = 'conv-employee-tab-label';
|
|
14437
|
-
label.textContent = persona.displayName || persona.key;
|
|
14438
|
-
const detail = document.createElement('small'); detail.className = 'conv-employee-tab-detail';
|
|
14439
|
-
detail.textContent = persona.role || 'AI Employee';
|
|
14440
|
-
copy.appendChild(label); copy.appendChild(detail);
|
|
14441
|
-
const addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.className = 'conv-employee-add';
|
|
14442
|
-
addBtn.textContent = '+';
|
|
14443
|
-
addBtn.title = 'Launch a job for ' + (persona.displayName || persona.key);
|
|
14444
|
-
addBtn.setAttribute('aria-label', 'Launch a job for ' + (persona.displayName || persona.key));
|
|
14445
|
-
addBtn.addEventListener('click', (e) => {
|
|
14446
|
-
e.preventDefault(); e.stopPropagation();
|
|
14447
|
-
// Launch from this area: palette prefiltered to the employee. startRun stamps
|
|
14448
|
-
// the active area on the conversation so it returns to the same rail.
|
|
14449
|
-
focusComposerForDelegate({ employeeId: (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.employeeId) || 'claude', prefixSearch: '/' + persona.key, personaKey: persona.key.startsWith('custom:') ? persona.key : undefined, actionContext: managementActionContext(area) });
|
|
14450
|
-
});
|
|
14451
|
-
const count = document.createElement('span'); count.className = 'conv-employee-tab-count';
|
|
14452
|
-
count.textContent = String(runs.length);
|
|
14453
|
-
summary.appendChild(avatar); summary.appendChild(copy); summary.appendChild(addBtn); summary.appendChild(count);
|
|
14454
|
-
details.appendChild(summary);
|
|
14455
|
-
if (!runs.length) {
|
|
14456
|
-
const listDiv = document.createElement('div'); listDiv.className = 'conv-employee-list';
|
|
14457
|
-
const hint = document.createElement('div'); hint.className = 'eh-empty';
|
|
14458
|
-
hint.textContent = 'No runs yet, + to launch a job.';
|
|
14459
|
-
listDiv.appendChild(hint);
|
|
14460
|
-
details.appendChild(listDiv);
|
|
14461
|
-
} else {
|
|
14462
|
-
details.appendChild(buildAreaGroupList(runs));
|
|
14463
|
-
}
|
|
14464
|
-
appendEmployeeGroupWithActions(rail, details);
|
|
14465
|
-
}
|
|
14466
|
-
|
|
14467
|
-
// #1397/#1398/#1606: shared employee rail for the Manager and Company tabs. Mirrors the
|
|
14468
|
-
// Projects-tab layout: In Progress (active runs) → per-employee accordions (completed
|
|
14469
|
-
// runs) → No FRAIM Employee group (completed null-persona runs). FRAIMworker is always
|
|
14470
|
-
// present via tfAreaRailPersonas; named employees join when the area is their home or
|
|
14471
|
-
// they have an actual run there.
|
|
14472
|
-
function tfRenderAreaEmployeeRail(rail, area, employeeGroupOpenState) {
|
|
14473
|
-
const allScopedConvs = tfAreaScopedConversations(area);
|
|
14474
|
-
const activeConvs = allScopedConvs.filter((c) => conversationUiState(c) !== 'complete');
|
|
14475
|
-
const doneNullPersonaConvs = allScopedConvs.filter(
|
|
14476
|
-
(c) => conversationUiState(c) === 'complete' && !c.personaKey
|
|
14477
|
-
);
|
|
14478
|
-
// Section 1: In Progress (active runs from all personas)
|
|
14479
|
-
const areaOpenFn = (key, defaultOpen) =>
|
|
14480
|
-
employeeGroupOpenState && employeeGroupOpenState.has(key)
|
|
14481
|
-
? employeeGroupOpenState.get(key)
|
|
14482
|
-
: defaultOpen;
|
|
14483
|
-
const ipDetails = buildInProgressSection(activeConvs.length, areaOpenFn);
|
|
14484
|
-
if (activeConvs.length > 0) {
|
|
14485
|
-
ipDetails.appendChild(buildAreaGroupList(activeConvs, { showEmployeeName: true }));
|
|
14486
|
-
} else {
|
|
14487
|
-
const emptyWrap = document.createElement('div'); emptyWrap.className = 'conv-employee-list';
|
|
14488
|
-
const ipEmpty = document.createElement('div'); ipEmpty.className = 'eh-empty';
|
|
14489
|
-
ipEmpty.textContent = 'Nothing running. Use + to assign a job below.';
|
|
14490
|
-
emptyWrap.appendChild(ipEmpty);
|
|
14491
|
-
ipDetails.appendChild(emptyWrap);
|
|
14492
|
-
}
|
|
14493
|
-
rail.appendChild(ipDetails);
|
|
14494
|
-
// Section 2: Per-employee accordions (completed runs, named personas)
|
|
14495
|
-
const areaPersonas = tfAreaRailPersonas(area);
|
|
14496
|
-
if (areaPersonas.length > 0) {
|
|
14497
|
-
const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Your employees';
|
|
14498
|
-
rail.appendChild(head);
|
|
14499
|
-
for (const persona of areaPersonas) {
|
|
14500
|
-
tfRenderAreaPersonaGroup(rail, persona, area, employeeGroupOpenState);
|
|
14501
|
-
}
|
|
14502
|
-
const pNote = document.createElement('div'); pNote.className = 'area-rail-note';
|
|
14503
|
-
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.";
|
|
14504
|
-
rail.appendChild(pNote);
|
|
14505
|
-
}
|
|
14506
|
-
// Section 3: No FRAIM Employee group (completed null-persona runs)
|
|
14507
|
-
if (doneNullPersonaConvs.length > 0) {
|
|
14508
|
-
const wcDetails = document.createElement('details');
|
|
14509
|
-
wcDetails.className = 'conv-employee-group conv-employee-group--adhoc';
|
|
14510
|
-
wcDetails.dataset.employeeKey = NO_FRAIM_EMPLOYEE_GROUP_KEY;
|
|
14511
|
-
wcDetails.open = employeeGroupOpenState && employeeGroupOpenState.has(NO_FRAIM_EMPLOYEE_GROUP_KEY)
|
|
14512
|
-
? employeeGroupOpenState.get(NO_FRAIM_EMPLOYEE_GROUP_KEY)
|
|
14513
|
-
: true;
|
|
14514
|
-
const wcSummary = document.createElement('summary');
|
|
14515
|
-
wcSummary.className = 'conv-employee-tab';
|
|
14516
|
-
const wcAvatar = buildNoFraimEmployeeAvatar('conv-employee-avatar');
|
|
14517
|
-
const wcCopy = document.createElement('span'); wcCopy.className = 'conv-employee-tab-copy';
|
|
14518
|
-
const wcLabel = document.createElement('strong'); wcLabel.className = 'conv-employee-tab-label';
|
|
14519
|
-
wcLabel.textContent = NO_FRAIM_EMPLOYEE_LABEL;
|
|
14520
|
-
wcCopy.appendChild(wcLabel);
|
|
14521
|
-
const wcCount = document.createElement('span'); wcCount.className = 'conv-employee-tab-count';
|
|
14522
|
-
wcCount.textContent = String(doneNullPersonaConvs.length);
|
|
14523
|
-
wcSummary.appendChild(wcAvatar); wcSummary.appendChild(wcCopy); wcSummary.appendChild(wcCount);
|
|
14524
|
-
wcDetails.appendChild(wcSummary);
|
|
14525
|
-
wcDetails.appendChild(buildAreaGroupList(doneNullPersonaConvs));
|
|
14526
|
-
rail.appendChild(wcDetails);
|
|
14527
|
-
}
|
|
14528
|
-
}
|
|
14529
|
-
|
|
14530
14448
|
// Move the shared .page conversation panel into the specified area's workspace-conv host.
|
|
14531
14449
|
// All .workspace-conv CSS rules (header/rail hidden, conv fills space) apply automatically
|
|
14532
14450
|
// because the area-conv-host elements carry the workspace-conv class.
|
|
@@ -14543,22 +14461,19 @@ function tfEnsurePageInArea(area) {
|
|
|
14543
14461
|
|
|
14544
14462
|
// Toggle a Company/Manager area between its info view (accordions) and conversation panel.
|
|
14545
14463
|
function tfToggleAreaView(area, view) {
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
tfEnsurePageInArea(
|
|
14552
|
-
|
|
14464
|
+
tf.areaSelection[tfAreaSelectionKey(area)] = view;
|
|
14465
|
+
if (typeof srchClear === 'function') srchClear();
|
|
14466
|
+
state.activeId = null;
|
|
14467
|
+
persistActiveConversationSelection(null, area === 'projects' ? { projectPath: state.projectPath || '' } : { scope: area });
|
|
14468
|
+
if (area === 'projects') {
|
|
14469
|
+
tfEnsurePageInArea(area);
|
|
14470
|
+
renderRail();
|
|
14553
14471
|
renderActive();
|
|
14554
|
-
|
|
14555
|
-
|
|
14556
|
-
|
|
14557
|
-
// the run view just because a conversation is still running. Overwritten by
|
|
14558
|
-
// tfStartOnboardingJob() when a genuinely new run starts in this area, or by the user
|
|
14559
|
-
// opening a run directly (switchToConversation).
|
|
14560
|
-
if (tf.areaSelection && (area === 'manager' || area === 'company')) tf.areaSelection[area] = 'info';
|
|
14472
|
+
tfRenderProjectContextTop();
|
|
14473
|
+
} else {
|
|
14474
|
+
tfRenderScopeView(area);
|
|
14561
14475
|
}
|
|
14476
|
+
tfApplyAreaView(area, tfResolveAreaView(area, null));
|
|
14562
14477
|
}
|
|
14563
14478
|
|
|
14564
14479
|
function tfRenderCompany() {
|
|
@@ -14571,32 +14486,7 @@ function tfRenderCompany() {
|
|
|
14571
14486
|
// closure-owned unsaved values. Save/Reset clears the guard below; the status
|
|
14572
14487
|
// state keeps their feedback stable if the next poll then performs a render.
|
|
14573
14488
|
if (!state.brandEditorDraftActive && typeof tfRenderBrandEditor === 'function') tfRenderBrandEditor();
|
|
14574
|
-
|
|
14575
|
-
if (rail) {
|
|
14576
|
-
// #1416: capture the current open/closed state before wiping the rail so the
|
|
14577
|
-
// ~1.5s poll-driven rebuild below doesn't snap a manually collapsed group back open.
|
|
14578
|
-
const employeeGroupOpenState = captureAreaEmployeeGroupOpenState(rail);
|
|
14579
|
-
rail.innerHTML = '';
|
|
14580
|
-
const orgConv = tfActiveOrgConv();
|
|
14581
|
-
const infoBtn = document.createElement('button');
|
|
14582
|
-
infoBtn.type = 'button';
|
|
14583
|
-
infoBtn.className = 'area-rail-info-btn' + (tfResolveAreaView('company', orgConv).info ? ' info-active' : '');
|
|
14584
|
-
infoBtn.textContent = '📋 Company Info';
|
|
14585
|
-
infoBtn.addEventListener('click', () => tfToggleAreaView('company', 'info'));
|
|
14586
|
-
rail.appendChild(infoBtn);
|
|
14587
|
-
appendManagementActions(rail, 'company');
|
|
14588
|
-
// Issue #1124: when a company-scoped org conv exists, add a visible rail entry so
|
|
14589
|
-
// the user can see and navigate to it. Previously the rail had only the static info
|
|
14590
|
-
// button and the conversation was unreachable from the left nav.
|
|
14591
|
-
// #693 R1 (PR round 2): the "Company jobs" launcher list is retired. Its jobs
|
|
14592
|
-
// now run from the section they populate — Run Organization Onboarding in
|
|
14593
|
-
// "Context & rules", Synthesize company learnings in "Company learnings".
|
|
14594
|
-
// #1398: FRAIMworker (and any hired employee with a company-scoped run, e.g. Aida)
|
|
14595
|
-
// get the same employee-group presentation as the Manager rail — not just an orphan
|
|
14596
|
-
// entry for the single "most active" org conv.
|
|
14597
|
-
tfRenderAreaEmployeeRail(rail, 'company', employeeGroupOpenState);
|
|
14598
|
-
tfReopenPendingRunDelete(rail);
|
|
14599
|
-
}
|
|
14489
|
+
renderScopeNavigation(document.getElementById('company-rail'), 'company');
|
|
14600
14490
|
const profile = document.getElementById('company-profile');
|
|
14601
14491
|
const learn = document.getElementById('company-learnings');
|
|
14602
14492
|
if (profile) {
|
|
@@ -14657,27 +14547,7 @@ function tfRenderCompany() {
|
|
|
14657
14547
|
// #693 R1 (PR round 2): Company's learnings job is organizational-learning-synthesis, not sleep-on-learnings.
|
|
14658
14548
|
tfEnsureAccAction('company-learn-acc', '🧠', 'Synthesize company learnings', () => tfStartOrgLearningSynthesis());
|
|
14659
14549
|
tfMaybeRenderOrgPushBanner();
|
|
14660
|
-
|
|
14661
|
-
// otherwise show the info view (accordions). This uses the shared .page element
|
|
14662
|
-
// moved via tfEnsurePageInArea() so renderActive() renders the full panel.
|
|
14663
|
-
const orgConv = tfActiveOrgConv();
|
|
14664
|
-
const host = document.getElementById('company-conv-host');
|
|
14665
|
-
const info = document.getElementById('company-info-view');
|
|
14666
|
-
// #1397: resolve the area's one stable destination — an explicit conversation choice,
|
|
14667
|
-
// an explicit "Company Info" choice, or (absent either) the org conv auto-home. Replaces
|
|
14668
|
-
// the former Info-only tf.viewPinned special case with one navigation-state contract.
|
|
14669
|
-
const companyView = tfResolveAreaView('company', orgConv);
|
|
14670
|
-
if (tf.area === 'company' && !companyView.info && (companyView.conversation || companyView.delegate)) {
|
|
14671
|
-
state.activeId = companyView.conversation ? companyView.conversation.id : null;
|
|
14672
|
-
tfEnsurePageInArea('company');
|
|
14673
|
-
if (host) host.hidden = false;
|
|
14674
|
-
if (info) info.hidden = true;
|
|
14675
|
-
renderActive();
|
|
14676
|
-
} else {
|
|
14677
|
-
if (host) host.hidden = true;
|
|
14678
|
-
if (info) info.hidden = false;
|
|
14679
|
-
if (tf.area === 'company') tfEnsurePageInArea('projects');
|
|
14680
|
-
}
|
|
14550
|
+
tfRenderScopeView('company');
|
|
14681
14551
|
}
|
|
14682
14552
|
|
|
14683
14553
|
// #594 R3: show push banner in Company area after an org onboarding run completes
|
|
@@ -14739,32 +14609,7 @@ function tfMaybeRenderOrgPushBanner() {
|
|
|
14739
14609
|
|
|
14740
14610
|
function tfRenderManager() {
|
|
14741
14611
|
// #521 R6: manager-level job (manager-agreements) lives ONLY in this left rail.
|
|
14742
|
-
|
|
14743
|
-
if (rail) {
|
|
14744
|
-
// #1416: capture the current open/closed state before wiping the rail so the
|
|
14745
|
-
// ~1s poll-driven rebuild below doesn't snap a manually collapsed group back open.
|
|
14746
|
-
const employeeGroupOpenState = captureAreaEmployeeGroupOpenState(rail);
|
|
14747
|
-
rail.innerHTML = '';
|
|
14748
|
-
const mgrConv = tfActiveMgrConv();
|
|
14749
|
-
const infoBtn = document.createElement('button');
|
|
14750
|
-
infoBtn.type = 'button';
|
|
14751
|
-
infoBtn.className = 'area-rail-info-btn' + (tfResolveAreaView('manager', mgrConv).info ? ' info-active' : '');
|
|
14752
|
-
infoBtn.textContent = '📋 Manager Info';
|
|
14753
|
-
infoBtn.addEventListener('click', () => tfToggleAreaView('manager', 'info'));
|
|
14754
|
-
rail.appendChild(infoBtn);
|
|
14755
|
-
appendManagementActions(rail, 'manager');
|
|
14756
|
-
// #693 R1 (PR round 2): the "Manager jobs" launcher list is retired.
|
|
14757
|
-
// Manager agreements now runs from the "Context & rules" section it populates.
|
|
14758
|
-
// Issue #702/#1398: render an EMPLOYEE GROUP for FRAIMworker plus every hired persona
|
|
14759
|
-
// that is either home to this area (Ashley on Manager) or has an actual run here (e.g.
|
|
14760
|
-
// Aida's manager-scoped runs) — the same avatar + name + runs + "+" presentation as the
|
|
14761
|
-
// Projects rail. `mgrConv` (the area's own top-level run, e.g. manager-agreements) gets
|
|
14762
|
-
// its own rail entry unless a group above already lists it.
|
|
14763
|
-
tfRenderAreaEmployeeRail(rail, 'manager', employeeGroupOpenState);
|
|
14764
|
-
// #824: the manager rail also attaches run-delete controls; re-attach an open
|
|
14765
|
-
// confirm this innerHTML='' rebuild would otherwise drop.
|
|
14766
|
-
tfReopenPendingRunDelete(rail);
|
|
14767
|
-
}
|
|
14612
|
+
renderScopeNavigation(document.getElementById('manager-rail'), 'manager');
|
|
14768
14613
|
const profile = document.getElementById('manager-profile');
|
|
14769
14614
|
const learn = document.getElementById('manager-learnings');
|
|
14770
14615
|
const reverse = document.getElementById('reverse-mentoring');
|
|
@@ -14856,25 +14701,7 @@ function tfRenderManager() {
|
|
|
14856
14701
|
// Cancel/Save/Check button handlers) call renderConfiguredAgentsPanel() itself,
|
|
14857
14702
|
// not this function, so they are unaffected and still render immediately.
|
|
14858
14703
|
if (!state.configuredAgentEditingId) renderConfiguredAgentsPanel();
|
|
14859
|
-
|
|
14860
|
-
// otherwise show the info view (accordions). Uses the shared .page element.
|
|
14861
|
-
const mgrConv = tfActiveMgrConv();
|
|
14862
|
-
const mgrHost = document.getElementById('manager-conv-host');
|
|
14863
|
-
const mgrInfo = document.getElementById('manager-info-view');
|
|
14864
|
-
// #1397: resolve the area's one stable destination — see tfRenderCompany's companyView
|
|
14865
|
-
// for the full rationale. Replaces the former Info-only tf.viewPinned special case.
|
|
14866
|
-
const managerView = tfResolveAreaView('manager', mgrConv);
|
|
14867
|
-
if (tf.area === 'manager' && !managerView.info && (managerView.conversation || managerView.delegate)) {
|
|
14868
|
-
state.activeId = managerView.conversation ? managerView.conversation.id : null;
|
|
14869
|
-
tfEnsurePageInArea('manager');
|
|
14870
|
-
if (mgrHost) mgrHost.hidden = false;
|
|
14871
|
-
if (mgrInfo) mgrInfo.hidden = true;
|
|
14872
|
-
renderActive();
|
|
14873
|
-
} else {
|
|
14874
|
-
if (mgrHost) mgrHost.hidden = true;
|
|
14875
|
-
if (mgrInfo) mgrInfo.hidden = false;
|
|
14876
|
-
if (tf.area === 'manager') tfEnsurePageInArea('projects');
|
|
14877
|
-
}
|
|
14704
|
+
tfRenderScopeView('manager');
|
|
14878
14705
|
}
|
|
14879
14706
|
// #533: Reverse mentoring now renders the real manager-coaching learnings via
|
|
14880
14707
|
// tfRenderLearningsList(scope='reverse') — consistent with the other learning
|
|
@@ -16131,10 +15958,10 @@ function tfApplyOrgBrand(brand) {
|
|
|
16131
15958
|
coEl.hidden = !show;
|
|
16132
15959
|
}
|
|
16133
15960
|
document.title = (state.orgBrand && state.orgBrand.name) ? state.orgBrand.name + ' Hub' : 'FRAIM AI Hub';
|
|
16134
|
-
//
|
|
15961
|
+
// Scope Info eyebrows reflect the company name (the FRAIM
|
|
16135
15962
|
// co-mark stays in the nav — co-branding, not white-label).
|
|
16136
15963
|
var eyebrow = (state.orgBrand && state.orgBrand.name) ? state.orgBrand.name : 'FRAIM Hub';
|
|
16137
|
-
['company-info-view', 'manager-info-view'].forEach(function (id) {
|
|
15964
|
+
['company-info-view', 'manager-info-view', 'proj-context-top'].forEach(function (id) {
|
|
16138
15965
|
var hostEl = document.getElementById(id);
|
|
16139
15966
|
var el = hostEl && hostEl.querySelector('.eyebrow');
|
|
16140
15967
|
if (el) el.textContent = eyebrow;
|
|
@@ -16414,8 +16241,8 @@ async function tfStartOnboardingJob(jobId, message, targetArea, userContext) {
|
|
|
16414
16241
|
// win over an earlier selection (Info view or a different conversation), otherwise the
|
|
16415
16242
|
// just-started run (e.g. create-hub-configured-agent) would stay invisible behind a
|
|
16416
16243
|
// stale manual choice. startRun() already set state.activeId to the new conversation.
|
|
16417
|
-
if (tf.areaSelection &&
|
|
16418
|
-
tf.areaSelection[targetArea] = state.activeId;
|
|
16244
|
+
if (tf.areaSelection && state.activeId) {
|
|
16245
|
+
tf.areaSelection[tfAreaSelectionKey(targetArea)] = state.activeId;
|
|
16419
16246
|
}
|
|
16420
16247
|
// After run creation refresh the area panel so the conversation becomes visible.
|
|
16421
16248
|
if (targetArea === 'company') tfRenderCompany();
|