fraim-hub 2.0.246 → 2.0.248
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.
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hostSessionState = exports.HostSessionState = void 0;
|
|
4
|
+
class HostSessionState {
|
|
5
|
+
keyFor(owner) {
|
|
6
|
+
const configuredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
|
|
7
|
+
return configuredAgentId || `${owner.baseHostId}-default`;
|
|
8
|
+
}
|
|
9
|
+
applySession(target, owner, sessionId, options = {}) {
|
|
10
|
+
const normalizedSessionId = sessionId.trim();
|
|
11
|
+
if (!normalizedSessionId)
|
|
12
|
+
return;
|
|
13
|
+
const at = options.at || new Date().toISOString();
|
|
14
|
+
const key = this.keyFor(owner);
|
|
15
|
+
target.hostSessions = {
|
|
16
|
+
...(target.hostSessions || {}),
|
|
17
|
+
[key]: {
|
|
18
|
+
configuredAgentId: owner.configuredAgentId || null,
|
|
19
|
+
baseHostId: owner.baseHostId,
|
|
20
|
+
sessionId: normalizedSessionId,
|
|
21
|
+
status: options.status || 'valid',
|
|
22
|
+
sourceRunId: options.sourceRunId ?? target.id ?? null,
|
|
23
|
+
updatedAt: at,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
target.sessionId = normalizedSessionId;
|
|
27
|
+
}
|
|
28
|
+
mergeFromRun(record, run) {
|
|
29
|
+
const owner = {
|
|
30
|
+
configuredAgentId: run.configuredAgentId || record.configuredAgentId || null,
|
|
31
|
+
baseHostId: (run.baseHostId || run.hostId || record.baseHostId || record.agentName),
|
|
32
|
+
};
|
|
33
|
+
record.hostSessions = { ...(record.hostSessions || {}) };
|
|
34
|
+
for (const [key, session] of Object.entries(run.hostSessions || {})) {
|
|
35
|
+
record.hostSessions[key] = { ...session };
|
|
36
|
+
}
|
|
37
|
+
if (run.sessionId) {
|
|
38
|
+
const projectedRun = {
|
|
39
|
+
id: run.id,
|
|
40
|
+
sessionId: record.sessionId || undefined,
|
|
41
|
+
hostSessions: record.hostSessions,
|
|
42
|
+
};
|
|
43
|
+
this.applySession(projectedRun, owner, run.sessionId, { sourceRunId: run.id });
|
|
44
|
+
record.hostSessions = projectedRun.hostSessions;
|
|
45
|
+
}
|
|
46
|
+
const active = this.resolve(record, owner);
|
|
47
|
+
record.sessionId = active?.sessionId || run.sessionId || record.sessionId || null;
|
|
48
|
+
}
|
|
49
|
+
resolve(conversation, owner) {
|
|
50
|
+
if (!conversation)
|
|
51
|
+
return null;
|
|
52
|
+
const sessions = conversation.hostSessions || {};
|
|
53
|
+
const key = this.keyFor(owner);
|
|
54
|
+
const exact = sessions[key];
|
|
55
|
+
if (this.isResumableForOwner(exact, owner))
|
|
56
|
+
return exact;
|
|
57
|
+
const fallback = Object.values(sessions).find((session) => this.isResumableForOwner(session, owner));
|
|
58
|
+
if (fallback)
|
|
59
|
+
return fallback;
|
|
60
|
+
const legacySessionId = typeof conversation.sessionId === 'string' ? conversation.sessionId.trim() : '';
|
|
61
|
+
if (!legacySessionId)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
configuredAgentId: owner.configuredAgentId || null,
|
|
65
|
+
baseHostId: owner.baseHostId,
|
|
66
|
+
sessionId: legacySessionId,
|
|
67
|
+
status: 'suspect',
|
|
68
|
+
sourceRunId: typeof conversation.runId === 'string' ? conversation.runId : null,
|
|
69
|
+
updatedAt: typeof conversation.lastUpdatedAt === 'string' ? conversation.lastUpdatedAt : new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
markInvalid(conversation, owner, sessionId, reason, at = new Date().toISOString()) {
|
|
73
|
+
const normalizedSessionId = sessionId.trim();
|
|
74
|
+
if (!normalizedSessionId)
|
|
75
|
+
return;
|
|
76
|
+
const key = this.keyFor(owner);
|
|
77
|
+
const existing = conversation.hostSessions?.[key];
|
|
78
|
+
conversation.hostSessions = { ...(conversation.hostSessions || {}) };
|
|
79
|
+
conversation.hostSessions[key] = {
|
|
80
|
+
configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
|
|
81
|
+
baseHostId: owner.baseHostId,
|
|
82
|
+
sessionId: normalizedSessionId,
|
|
83
|
+
status: 'invalid',
|
|
84
|
+
sourceRunId: existing?.sourceRunId || (typeof conversation.runId === 'string' ? conversation.runId : null),
|
|
85
|
+
updatedAt: existing?.updatedAt || at,
|
|
86
|
+
invalidatedAt: at,
|
|
87
|
+
invalidationReason: reason,
|
|
88
|
+
};
|
|
89
|
+
if (conversation.sessionId === normalizedSessionId) {
|
|
90
|
+
const replacement = this.resolve({ ...conversation, sessionId: null }, owner);
|
|
91
|
+
conversation.sessionId = replacement?.sessionId || null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
markInvalidRun(run, owner, sessionId, reason, at = new Date().toISOString()) {
|
|
95
|
+
const normalizedSessionId = sessionId.trim();
|
|
96
|
+
if (!normalizedSessionId)
|
|
97
|
+
return;
|
|
98
|
+
const key = this.keyFor(owner);
|
|
99
|
+
const existing = run.hostSessions?.[key];
|
|
100
|
+
run.hostSessions = { ...(run.hostSessions || {}) };
|
|
101
|
+
run.hostSessions[key] = {
|
|
102
|
+
configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
|
|
103
|
+
baseHostId: owner.baseHostId,
|
|
104
|
+
sessionId: normalizedSessionId,
|
|
105
|
+
status: 'invalid',
|
|
106
|
+
sourceRunId: existing?.sourceRunId || run.id || null,
|
|
107
|
+
updatedAt: existing?.updatedAt || at,
|
|
108
|
+
invalidatedAt: at,
|
|
109
|
+
invalidationReason: reason,
|
|
110
|
+
};
|
|
111
|
+
if (run.sessionId === normalizedSessionId) {
|
|
112
|
+
run.sessionId = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
hasInvalidSession(conversation, owner, sessionId) {
|
|
116
|
+
const normalizedSessionId = sessionId.trim();
|
|
117
|
+
if (!conversation || !normalizedSessionId)
|
|
118
|
+
return false;
|
|
119
|
+
const key = this.keyFor(owner);
|
|
120
|
+
const exact = conversation.hostSessions?.[key];
|
|
121
|
+
if (exact?.status === 'invalid' && exact.sessionId === normalizedSessionId)
|
|
122
|
+
return true;
|
|
123
|
+
return Object.values(conversation.hostSessions || {}).some((session) => (session.status === 'invalid'
|
|
124
|
+
&& session.sessionId === normalizedSessionId
|
|
125
|
+
&& session.baseHostId === owner.baseHostId));
|
|
126
|
+
}
|
|
127
|
+
isResumableForOwner(session, owner) {
|
|
128
|
+
if (!session || !session.sessionId || session.status === 'invalid')
|
|
129
|
+
return false;
|
|
130
|
+
if (session.baseHostId !== owner.baseHostId)
|
|
131
|
+
return false;
|
|
132
|
+
const requestedConfiguredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
|
|
133
|
+
const sessionConfiguredAgentId = typeof session.configuredAgentId === 'string' ? session.configuredAgentId.trim() : '';
|
|
134
|
+
return !requestedConfiguredAgentId || !sessionConfiguredAgentId || requestedConfiguredAgentId === sessionConfiguredAgentId;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.HostSessionState = HostSessionState;
|
|
138
|
+
exports.hostSessionState = new HostSessionState();
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -1578,7 +1578,10 @@ function parseHostLine(hostId, line) {
|
|
|
1578
1578
|
if (parsed.type === 'result') {
|
|
1579
1579
|
// Don't emit message — the 'assistant' event already captured the turn text.
|
|
1580
1580
|
// result carries usage data (parsed by parseUsageSignal above via withSignal).
|
|
1581
|
-
|
|
1581
|
+
// Claude result events can echo the requested resume id even when resume failed
|
|
1582
|
+
// with "No conversation found with session ID". Treat system events as the
|
|
1583
|
+
// authoritative session source instead of reinforcing a stale pointer.
|
|
1584
|
+
return withSignal({ raw: trimmed });
|
|
1582
1585
|
}
|
|
1583
1586
|
return withSignal({ raw: trimmed });
|
|
1584
1587
|
}
|
|
@@ -112,16 +112,15 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
|
112
112
|
const byPath = new Map();
|
|
113
113
|
const currentCanonical = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
|
|
114
114
|
const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths));
|
|
115
|
-
const add = (entry
|
|
115
|
+
const add = (entry) => {
|
|
116
116
|
if (!entry)
|
|
117
117
|
return;
|
|
118
118
|
const displayPath = normalizeProjectPath(entry.folderPath);
|
|
119
119
|
const dedupKey = canonicalProjectPath(displayPath);
|
|
120
|
-
// Issue #
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
if (!isCurrent && removedKeys.has(dedupKey))
|
|
120
|
+
// Issue #1044: tombstoned paths are never shown, even when the path is the active
|
|
121
|
+
// project. The caller must switch to a different project (or clear projectPath) before
|
|
122
|
+
// deleting — the server now enforces this only when other projects remain (#719 reverted).
|
|
123
|
+
if (removedKeys.has(dedupKey))
|
|
125
124
|
return;
|
|
126
125
|
if (!options.includeMissing && dedupKey !== currentCanonical && !projectPathExists(displayPath))
|
|
127
126
|
return;
|
|
@@ -129,7 +128,7 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
|
129
128
|
byPath.set(dedupKey, existing ? { ...existing, ...entry, folderPath: existing.folderPath } : { ...entry, folderPath: displayPath });
|
|
130
129
|
};
|
|
131
130
|
if (currentProjectPath)
|
|
132
|
-
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath)
|
|
131
|
+
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath));
|
|
133
132
|
for (const project of projects)
|
|
134
133
|
add(normalizeProjectEntry(project));
|
|
135
134
|
return withUniqueProjectIds(Array.from(byPath.values()));
|
|
@@ -55,6 +55,7 @@ const custom_employees_1 = require("./custom-employees");
|
|
|
55
55
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
56
56
|
const hosts_1 = require("./hosts");
|
|
57
57
|
const configured_agents_1 = require("./configured-agents");
|
|
58
|
+
const host_session_state_1 = require("./host-session-state");
|
|
58
59
|
const url_safety_1 = require("./url-safety");
|
|
59
60
|
const manager_turns_1 = require("./manager-turns");
|
|
60
61
|
const preferences_1 = require("./preferences");
|
|
@@ -117,6 +118,10 @@ function buildReviewApprovalSystemEventText(instructions) {
|
|
|
117
118
|
return 'review_approved send_delivery cleanup_branch';
|
|
118
119
|
return null;
|
|
119
120
|
}
|
|
121
|
+
function extractMissingHostSessionId(text) {
|
|
122
|
+
const match = text.match(/No conversation found with session ID:\s*([a-zA-Z0-9-]+)/i);
|
|
123
|
+
return match?.[1]?.trim() || null;
|
|
124
|
+
}
|
|
120
125
|
function loadManagerHiringModule() {
|
|
121
126
|
const cached = managerHiringModule;
|
|
122
127
|
if (cached !== undefined) {
|
|
@@ -1444,7 +1449,10 @@ function ensureDirectoryPath(projectPath) {
|
|
|
1444
1449
|
}
|
|
1445
1450
|
return resolved;
|
|
1446
1451
|
}
|
|
1447
|
-
function readRunScope(raw) {
|
|
1452
|
+
function readRunScope(raw, jobId) {
|
|
1453
|
+
if ((raw === undefined || raw === null) && jobId && MACHINE_LEVEL_JOB_IDS.has(jobId)) {
|
|
1454
|
+
return 'manager';
|
|
1455
|
+
}
|
|
1448
1456
|
return raw === 'manager' || raw === 'company' ? raw : 'project';
|
|
1449
1457
|
}
|
|
1450
1458
|
// Working directory for a project-independent run when no project exists: the user-level
|
|
@@ -1612,7 +1620,7 @@ function classifyExit(run, exitCode) {
|
|
|
1612
1620
|
if (lastDeclared && lastDeclared.id === currentPhase) {
|
|
1613
1621
|
return { action: 'done', pauseReason: 'done' };
|
|
1614
1622
|
}
|
|
1615
|
-
return { action: '
|
|
1623
|
+
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
1616
1624
|
}
|
|
1617
1625
|
// Phase mid-flight (started but not completed/incomplete): conservative park.
|
|
1618
1626
|
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
@@ -2229,7 +2237,7 @@ class AiHubServer {
|
|
|
2229
2237
|
conversationRecordFromRun(run) {
|
|
2230
2238
|
const lastUpdatedAt = run.updatedAt || new Date().toISOString();
|
|
2231
2239
|
const stages = deriveStages(run, run.projectPath);
|
|
2232
|
-
|
|
2240
|
+
const record = {
|
|
2233
2241
|
id: run.conversationId || run.id,
|
|
2234
2242
|
projectPath: path_1.default.resolve(run.projectPath),
|
|
2235
2243
|
title: run.conversationTitle || run.jobTitle || run.jobId,
|
|
@@ -2279,6 +2287,7 @@ class AiHubServer {
|
|
|
2279
2287
|
handoffSummary: run.handoffSummary || null,
|
|
2280
2288
|
continuityDecision: run.continuityDecision,
|
|
2281
2289
|
forkedFromConversationId: run.forkedFromConversationId ?? null,
|
|
2290
|
+
hostSessions: { ...(run.hostSessions || {}) },
|
|
2282
2291
|
restartRecovery: run.restartRecovery || null,
|
|
2283
2292
|
restartRecoverySkippedReason: run.restartRecoverySkippedReason || null,
|
|
2284
2293
|
// Issue #578: preserve trigger source so the UI can render the chip.
|
|
@@ -2296,6 +2305,8 @@ class AiHubServer {
|
|
|
2296
2305
|
runDiscriminant: run.runDiscriminant || null,
|
|
2297
2306
|
},
|
|
2298
2307
|
};
|
|
2308
|
+
host_session_state_1.hostSessionState.mergeFromRun(record, run);
|
|
2309
|
+
return record;
|
|
2299
2310
|
}
|
|
2300
2311
|
persistRunConversationNow(run, activeId) {
|
|
2301
2312
|
try {
|
|
@@ -2344,7 +2355,7 @@ class AiHubServer {
|
|
|
2344
2355
|
onEvent: (event, channel) => {
|
|
2345
2356
|
this.runRegistry.update(run.id, (current) => {
|
|
2346
2357
|
if (event.sessionId) {
|
|
2347
|
-
current.
|
|
2358
|
+
host_session_state_1.hostSessionState.applySession(current, { configuredAgentId: current.configuredAgentId || null, baseHostId: current.baseHostId || current.hostId }, event.sessionId, { sourceRunId: current.id });
|
|
2348
2359
|
current.resumeCommand = (0, hosts_1.buildInteractiveResumeCommand)(run.hostId, event.sessionId);
|
|
2349
2360
|
}
|
|
2350
2361
|
appendHostMessage(current, run.hostId, event, channel);
|
|
@@ -3906,7 +3917,7 @@ class AiHubServer {
|
|
|
3906
3917
|
onEvent: (event, channel) => {
|
|
3907
3918
|
this.runRegistry.update(run.id, (current) => {
|
|
3908
3919
|
if (event.sessionId) {
|
|
3909
|
-
current.
|
|
3920
|
+
host_session_state_1.hostSessionState.applySession(current, { configuredAgentId: current.configuredAgentId || null, baseHostId: current.baseHostId || current.hostId }, event.sessionId, { sourceRunId: current.id });
|
|
3910
3921
|
current.resumeCommand = (0, hosts_1.buildInteractiveResumeCommand)(run.hostId, event.sessionId);
|
|
3911
3922
|
}
|
|
3912
3923
|
appendHostMessage(current, run.hostId, event, channel);
|
|
@@ -4039,10 +4050,14 @@ class AiHubServer {
|
|
|
4039
4050
|
|| (requestedFolderPath ? known.find((project) => sameDirectoryPath(project.folderPath, requestedFolderPath)) : undefined);
|
|
4040
4051
|
if (!entry)
|
|
4041
4052
|
return res.status(404).json({ error: 'Project not found.' });
|
|
4042
|
-
//
|
|
4043
|
-
//
|
|
4044
|
-
//
|
|
4045
|
-
|
|
4053
|
+
// Issue #1044: when the project being removed is the active workspace AND other
|
|
4054
|
+
// projects remain, the client must switch first (the current path is re-injected
|
|
4055
|
+
// on every load, so it would resurface). When it is the LAST project, allow the
|
|
4056
|
+
// delete and let the Hub reach an empty state — projectPath is cleared by the
|
|
4057
|
+
// tombstone filter (normalizeAiHubProjectList no longer bypasses for isCurrent).
|
|
4058
|
+
const isCurrentProject = sameDirectoryPath(entry.folderPath, projectPath);
|
|
4059
|
+
const otherProjectsExist = known.some((p) => p.id !== entry.id);
|
|
4060
|
+
if (isCurrentProject && otherProjectsExist) {
|
|
4046
4061
|
return res.status(409).json({ error: 'Cannot remove the current project. Switch to another project first.' });
|
|
4047
4062
|
}
|
|
4048
4063
|
// A live schedule/webhook would fire later, write a conversation under the
|
|
@@ -4452,7 +4467,7 @@ class AiHubServer {
|
|
|
4452
4467
|
try {
|
|
4453
4468
|
// Issue #892: project-independent (manager/company) runs resolve a working dir
|
|
4454
4469
|
// even with no project; project runs still require an existing project directory.
|
|
4455
|
-
const scope = readRunScope(req.body.scope);
|
|
4470
|
+
const scope = readRunScope(req.body.scope, req.body.jobId);
|
|
4456
4471
|
const projectPath = this.resolveRunProjectPath(req.body.projectPath, scope);
|
|
4457
4472
|
const requestedHostId = req.body.hostId;
|
|
4458
4473
|
const instructions = (req.body.instructions || '').trim();
|
|
@@ -4877,14 +4892,14 @@ class AiHubServer {
|
|
|
4877
4892
|
const body = (req.body ?? {});
|
|
4878
4893
|
// Issue #892: a mid-flight project-independent onboarding (manager/company) must
|
|
4879
4894
|
// resume even with no project (e.g. after a Hub restart).
|
|
4880
|
-
const scope = readRunScope(body.scope);
|
|
4895
|
+
const scope = readRunScope(body.scope, body.jobId);
|
|
4881
4896
|
const projectPath = this.resolveRunProjectPath(body.projectPath, scope);
|
|
4882
4897
|
const requestedHostId = body.hostId;
|
|
4883
|
-
const
|
|
4898
|
+
const requestedSessionId = (body.sessionId || '').trim();
|
|
4884
4899
|
const jobId = (body.jobId || '').trim();
|
|
4885
4900
|
const instructions = (body.instructions || '').trim();
|
|
4886
4901
|
const coachingJobId = body.coachingJobId?.trim() || undefined;
|
|
4887
|
-
if (!
|
|
4902
|
+
if (!requestedSessionId)
|
|
4888
4903
|
throw new Error('A host sessionId is required to resume.');
|
|
4889
4904
|
if (!jobId)
|
|
4890
4905
|
throw new Error('A jobId is required to resume.');
|
|
@@ -4904,6 +4919,17 @@ class AiHubServer {
|
|
|
4904
4919
|
? this.conversationStore.loadProject(conversationBucketKey).conversations.find((entry) => entry.id === requestedConversationId)
|
|
4905
4920
|
: inferredConversation ?? undefined;
|
|
4906
4921
|
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
4922
|
+
const resolvedHostSession = host_session_state_1.hostSessionState.resolve(persistedConversation, {
|
|
4923
|
+
configuredAgentId: configuredAgent.id,
|
|
4924
|
+
baseHostId: hostId,
|
|
4925
|
+
});
|
|
4926
|
+
if (!resolvedHostSession && host_session_state_1.hostSessionState.hasInvalidSession(persistedConversation, { configuredAgentId: configuredAgent.id, baseHostId: hostId }, requestedSessionId)) {
|
|
4927
|
+
return res.status(409).json({
|
|
4928
|
+
error: 'The saved host session was already marked invalid. Start a fresh handoff-backed run instead of retrying the same session.',
|
|
4929
|
+
hostSessionStatus: 'invalid',
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4932
|
+
const sessionId = resolvedHostSession?.sessionId || requestedSessionId;
|
|
4907
4933
|
const now = new Date().toISOString();
|
|
4908
4934
|
const run = {
|
|
4909
4935
|
id: (0, crypto_1.randomUUID)(),
|
|
@@ -4911,6 +4937,7 @@ class AiHubServer {
|
|
|
4911
4937
|
conversationTitle: typeof body.conversationTitle === 'string' && body.conversationTitle.trim() ? body.conversationTitle.trim() : undefined,
|
|
4912
4938
|
jobTitle: typeof body.jobTitle === 'string' && body.jobTitle.trim() ? body.jobTitle.trim() : jobId,
|
|
4913
4939
|
jobId, hostId, configuredAgentId: configuredAgent.id, configuredAgentLabel: configuredAgent.label, baseHostId: configuredAgent.baseHostId, projectPath, status: 'running', sessionId,
|
|
4940
|
+
hostSessions: persistedConversation?.hostSessions ? { ...persistedConversation.hostSessions } : undefined,
|
|
4914
4941
|
// Issue #892: keep the invocation scope so a resumed manager/company run stays
|
|
4915
4942
|
// in its project-independent conversation bucket.
|
|
4916
4943
|
scope,
|
|
@@ -4927,6 +4954,7 @@ class AiHubServer {
|
|
|
4927
4954
|
personaKey: getHubPersonaForJob(jobId),
|
|
4928
4955
|
continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
|
|
4929
4956
|
};
|
|
4957
|
+
host_session_state_1.hostSessionState.applySession(run, { configuredAgentId: configuredAgent.id, baseHostId: hostId }, sessionId, { sourceRunId: run.id, status: resolvedHostSession?.status || 'suspect' });
|
|
4930
4958
|
// Continue-turn message (FRAIM invocation for the job + instructions) plus
|
|
4931
4959
|
// the shared-browser note so the resumed agent knows about it.
|
|
4932
4960
|
const preparedResume = this.prepareContinueMessage(run, instructions, coachingJobId);
|
|
@@ -4945,11 +4973,15 @@ class AiHubServer {
|
|
|
4945
4973
|
onEvent: (event, channel) => {
|
|
4946
4974
|
this.runRegistry.update(run.id, (current) => {
|
|
4947
4975
|
if (event.sessionId) {
|
|
4948
|
-
current.
|
|
4976
|
+
host_session_state_1.hostSessionState.applySession(current, { configuredAgentId: current.configuredAgentId || null, baseHostId: current.baseHostId || current.hostId }, event.sessionId, { sourceRunId: current.id });
|
|
4949
4977
|
current.resumeCommand = (0, hosts_1.buildInteractiveResumeCommand)(hostId, event.sessionId);
|
|
4950
4978
|
}
|
|
4951
4979
|
appendHostMessage(current, hostId, event, channel);
|
|
4952
4980
|
if (event.raw) {
|
|
4981
|
+
const missingSessionId = extractMissingHostSessionId(event.raw);
|
|
4982
|
+
if (missingSessionId) {
|
|
4983
|
+
host_session_state_1.hostSessionState.markInvalidRun(current, { configuredAgentId: current.configuredAgentId || null, baseHostId: current.baseHostId || current.hostId }, missingSessionId, 'host-reported-missing-session');
|
|
4984
|
+
}
|
|
4953
4985
|
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
4954
4986
|
applyReviewProjection(current, event.raw);
|
|
4955
4987
|
}
|
|
@@ -5446,14 +5478,23 @@ class AiHubServer {
|
|
|
5446
5478
|
return res.status(404).json({ error: `Custom employee ${key} not found.` });
|
|
5447
5479
|
}
|
|
5448
5480
|
const { displayName, role, icon, jobIds } = req.body;
|
|
5481
|
+
if (displayName !== undefined && (typeof displayName !== 'string' || !displayName.trim())) {
|
|
5482
|
+
return res.status(400).json({ error: 'displayName is required.' });
|
|
5483
|
+
}
|
|
5484
|
+
if (jobIds !== undefined && (!Array.isArray(jobIds) || jobIds.length === 0)) {
|
|
5485
|
+
return res.status(400).json({ error: 'jobIds must be a non-empty array.' });
|
|
5486
|
+
}
|
|
5487
|
+
const nextDisplayName = displayName !== undefined ? displayName.trim() : record.displayName;
|
|
5449
5488
|
const VALID_ICON_KINDS_PATCH = new Set(['emoji', 'generated', 'image']);
|
|
5450
5489
|
const patchedIcon = icon !== undefined
|
|
5451
5490
|
? { kind: (VALID_ICON_KINDS_PATCH.has(icon.kind) ? icon.kind : record.icon.kind), value: icon.value ?? record.icon.value }
|
|
5452
|
-
: undefined
|
|
5491
|
+
: (displayName !== undefined && record.icon.kind === 'generated')
|
|
5492
|
+
? { ...record.icon, value: nextDisplayName }
|
|
5493
|
+
: undefined;
|
|
5453
5494
|
const updated = {
|
|
5454
5495
|
...record,
|
|
5455
|
-
...(displayName !== undefined ? { displayName } : {}),
|
|
5456
|
-
...(role !== undefined ? { role } : {}),
|
|
5496
|
+
...(displayName !== undefined ? { displayName: nextDisplayName } : {}),
|
|
5497
|
+
...(role !== undefined ? { role: role.trim() } : {}),
|
|
5457
5498
|
...(patchedIcon !== undefined ? { icon: patchedIcon } : {}),
|
|
5458
5499
|
...(jobIds !== undefined ? { jobIds } : {}),
|
|
5459
5500
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.248",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"electron": "^41.2.2",
|
|
162
162
|
"electron-updater": "^6.8.9",
|
|
163
163
|
"express": "^5.2.1",
|
|
164
|
-
"fraim": "2.0.
|
|
164
|
+
"fraim": "2.0.248",
|
|
165
165
|
"mongodb": "^7.0.0",
|
|
166
166
|
"node-cron": "4.2.1",
|
|
167
167
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -67,6 +67,7 @@ const state = {
|
|
|
67
67
|
pendingConnectedSurfaceAfterAuth: null,
|
|
68
68
|
panelState: {}, // { [convId]: { coach?: boolean } }
|
|
69
69
|
wordContext: null, // WordContext pushed from taskpane.html via postMessage
|
|
70
|
+
taskPaneProjects: [],
|
|
70
71
|
// Pending coaching job selected via a quick-coach button or template picker.
|
|
71
72
|
// Sent as coachingJobId in the next continueRun call and then cleared.
|
|
72
73
|
// The invocation prefix ($fraim / /fraim) is added server-side based on
|
|
@@ -248,6 +249,13 @@ function applyBootstrap(bootstrap, docUrl) {
|
|
|
248
249
|
bootstrap = mergePersonaProjection(bootstrap, state.bootstrap);
|
|
249
250
|
state.bootstrap = bootstrap;
|
|
250
251
|
state.projectPath = bootstrap.project.path;
|
|
252
|
+
// Issue #1044: seed client-side tombstone set from server preferences so
|
|
253
|
+
// tfEnsureCurrentProject does not re-inject a path the user just deleted.
|
|
254
|
+
if (bootstrap.preferences && Array.isArray(bootstrap.preferences.removedProjectPaths)) {
|
|
255
|
+
for (const p of bootstrap.preferences.removedProjectPaths) {
|
|
256
|
+
if (typeof p === 'string' && p.length > 0) tf.removedPaths.add(tfCanonicalProjectPath(p));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
251
259
|
state.remoteBaseUrl = bootstrap.remoteBaseUrl || null;
|
|
252
260
|
if (bootstrap.preferences && bootstrap.preferences.apiKey) {
|
|
253
261
|
state.storedApiKey = bootstrap.preferences.apiKey;
|
|
@@ -420,6 +428,7 @@ function applyWordContext(ctx, partial) {
|
|
|
420
428
|
}
|
|
421
429
|
renderWordContextBar();
|
|
422
430
|
renderWordContextInModal();
|
|
431
|
+
renderTaskPaneLauncher();
|
|
423
432
|
}
|
|
424
433
|
|
|
425
434
|
function setupWordBridge() {
|
|
@@ -520,6 +529,217 @@ function renderWordContextInModal() {
|
|
|
520
529
|
body.textContent = buildWordContextBlock(wc);
|
|
521
530
|
}
|
|
522
531
|
|
|
532
|
+
function isTaskPaneSurface() {
|
|
533
|
+
return document.body.dataset.surface === 'task-pane' || document.body.dataset.surface === 'extension';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function taskPaneProjectEntries() {
|
|
537
|
+
const byPath = new Map();
|
|
538
|
+
const add = (project) => {
|
|
539
|
+
if (!project) return;
|
|
540
|
+
const folderPath = project.folderPath || project.path || project.folder || '';
|
|
541
|
+
if (!folderPath || byPath.has(tfCanonicalProjectPath(folderPath))) return;
|
|
542
|
+
byPath.set(tfCanonicalProjectPath(folderPath), {
|
|
543
|
+
...project,
|
|
544
|
+
folderPath,
|
|
545
|
+
name: project.name || friendlyProjectShortName(folderPath),
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
add({ folderPath: state.projectPath, name: friendlyProjectShortName(state.projectPath) });
|
|
549
|
+
(state.taskPaneProjects || []).forEach(add);
|
|
550
|
+
((state.bootstrap && state.bootstrap.projects) || []).forEach(add);
|
|
551
|
+
return Array.from(byPath.values());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function taskPaneJobEntries() {
|
|
555
|
+
const jobs = [
|
|
556
|
+
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
557
|
+
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
558
|
+
];
|
|
559
|
+
const seen = new Set();
|
|
560
|
+
return jobs
|
|
561
|
+
.filter((job) => job && job.id && !PAGE_SCOPED_JOBS.has(job.id))
|
|
562
|
+
.filter((job) => {
|
|
563
|
+
if (seen.has(job.id)) return false;
|
|
564
|
+
seen.add(job.id);
|
|
565
|
+
return true;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function taskPaneInstructionSeed() {
|
|
570
|
+
const wc = state.wordContext || {};
|
|
571
|
+
if (wc.selection) return wc.selection;
|
|
572
|
+
if (wc.bodyPreview) return wc.bodyPreview.slice(0, 500);
|
|
573
|
+
return '';
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function taskPaneSetOptions(select, entries, valueOf, labelOf, selectedValue) {
|
|
577
|
+
if (!select) return;
|
|
578
|
+
const previous = select.value;
|
|
579
|
+
select.innerHTML = '';
|
|
580
|
+
entries.forEach((entry) => {
|
|
581
|
+
const opt = document.createElement('option');
|
|
582
|
+
opt.value = valueOf(entry);
|
|
583
|
+
opt.textContent = labelOf(entry);
|
|
584
|
+
select.appendChild(opt);
|
|
585
|
+
});
|
|
586
|
+
const wanted = selectedValue || previous;
|
|
587
|
+
if (wanted && entries.some((entry) => valueOf(entry) === wanted)) select.value = wanted;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function ensureTaskPaneLauncher() {
|
|
591
|
+
let root = document.getElementById('task-pane-launcher');
|
|
592
|
+
if (root) return root;
|
|
593
|
+
root = document.createElement('section');
|
|
594
|
+
root.id = 'task-pane-launcher';
|
|
595
|
+
root.className = 'task-pane-launcher';
|
|
596
|
+
|
|
597
|
+
const header = document.createElement('div');
|
|
598
|
+
header.className = 'tp-launcher-header';
|
|
599
|
+
const title = document.createElement('div');
|
|
600
|
+
title.className = 'tp-launcher-title';
|
|
601
|
+
title.textContent = 'Delegate from Word';
|
|
602
|
+
const context = document.createElement('div');
|
|
603
|
+
context.id = 'task-pane-context-summary';
|
|
604
|
+
context.className = 'tp-launcher-context';
|
|
605
|
+
header.appendChild(title);
|
|
606
|
+
header.appendChild(context);
|
|
607
|
+
root.appendChild(header);
|
|
608
|
+
|
|
609
|
+
const fields = [
|
|
610
|
+
['Project', 'task-pane-project-select', 'select'],
|
|
611
|
+
['Agent tool', 'task-pane-employee-select', 'select'],
|
|
612
|
+
['Job', 'task-pane-job-select', 'select'],
|
|
613
|
+
['Instructions', 'task-pane-instructions', 'textarea'],
|
|
614
|
+
];
|
|
615
|
+
fields.forEach(([labelText, id, tag]) => {
|
|
616
|
+
const label = document.createElement('label');
|
|
617
|
+
label.className = 'tp-launcher-field';
|
|
618
|
+
label.setAttribute('for', id);
|
|
619
|
+
const span = document.createElement('span');
|
|
620
|
+
span.textContent = labelText;
|
|
621
|
+
const input = document.createElement(tag);
|
|
622
|
+
input.id = id;
|
|
623
|
+
if (tag === 'textarea') {
|
|
624
|
+
input.rows = 4;
|
|
625
|
+
input.placeholder = 'Add the outcome you want from this Word context.';
|
|
626
|
+
input.addEventListener('input', () => { input.dataset.userTouched = '1'; });
|
|
627
|
+
}
|
|
628
|
+
label.appendChild(span);
|
|
629
|
+
label.appendChild(input);
|
|
630
|
+
root.appendChild(label);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const actions = document.createElement('div');
|
|
634
|
+
actions.className = 'tp-launcher-actions';
|
|
635
|
+
const status = document.createElement('div');
|
|
636
|
+
status.id = 'task-pane-start-status';
|
|
637
|
+
status.className = 'tp-launcher-status';
|
|
638
|
+
const start = document.createElement('button');
|
|
639
|
+
start.id = 'task-pane-start-job';
|
|
640
|
+
start.className = 'send-button';
|
|
641
|
+
start.type = 'button';
|
|
642
|
+
start.textContent = 'Start job';
|
|
643
|
+
start.addEventListener('click', taskPaneStartSelectedJob);
|
|
644
|
+
actions.appendChild(status);
|
|
645
|
+
actions.appendChild(start);
|
|
646
|
+
root.appendChild(actions);
|
|
647
|
+
|
|
648
|
+
document.body.insertBefore(root, document.body.firstChild);
|
|
649
|
+
document.getElementById('task-pane-project-select')?.addEventListener('change', (event) => {
|
|
650
|
+
const statusEl = document.getElementById('task-pane-start-status');
|
|
651
|
+
const project = taskPaneProjectEntries().find((entry) => entry.folderPath === event.target.value);
|
|
652
|
+
if (statusEl && project) statusEl.textContent = `Project: ${project.name || friendlyProjectShortName(project.folderPath)}`;
|
|
653
|
+
});
|
|
654
|
+
return root;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function renderTaskPaneLauncher() {
|
|
658
|
+
if (!isTaskPaneSurface() || !state.bootstrap) return;
|
|
659
|
+
ensureTaskPaneLauncher();
|
|
660
|
+
const projects = taskPaneProjectEntries();
|
|
661
|
+
const jobs = taskPaneJobEntries();
|
|
662
|
+
const agents = hubConfiguredAgents().filter((agent) => agent.available !== false && agent.enabled !== false);
|
|
663
|
+
const projectSelect = document.getElementById('task-pane-project-select');
|
|
664
|
+
const employeeSelect = document.getElementById('task-pane-employee-select');
|
|
665
|
+
const jobSelect = document.getElementById('task-pane-job-select');
|
|
666
|
+
const instructions = document.getElementById('task-pane-instructions');
|
|
667
|
+
const start = document.getElementById('task-pane-start-job');
|
|
668
|
+
const context = document.getElementById('task-pane-context-summary');
|
|
669
|
+
|
|
670
|
+
taskPaneSetOptions(projectSelect, projects, (project) => project.folderPath, (project) => project.name || friendlyProjectShortName(project.folderPath), state.projectPath);
|
|
671
|
+
taskPaneSetOptions(employeeSelect, agents, (agent) => agent.id, (agent) => agent.label || agent.id, state.selectedEmployeeId);
|
|
672
|
+
taskPaneSetOptions(jobSelect, jobs, (job) => job.id, (job) => job.title || job.id, null);
|
|
673
|
+
|
|
674
|
+
if (instructions && instructions.dataset.userTouched !== '1') {
|
|
675
|
+
instructions.value = taskPaneInstructionSeed();
|
|
676
|
+
}
|
|
677
|
+
if (context) {
|
|
678
|
+
const wc = state.wordContext || {};
|
|
679
|
+
const projectName = friendlyProjectShortName(state.projectPath);
|
|
680
|
+
const selected = wc.selection ? wc.selection.split(/\s+/).filter(Boolean).length + ' selected words' : 'Word context ready';
|
|
681
|
+
context.textContent = projectName ? `${projectName} - ${selected}` : selected;
|
|
682
|
+
}
|
|
683
|
+
if (start) start.disabled = projects.length === 0 || agents.length === 0 || jobs.length === 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function refreshTaskPaneProjects() {
|
|
687
|
+
if (!isTaskPaneSurface()) return;
|
|
688
|
+
const query = state.projectPath ? '?projectPath=' + encodeURIComponent(state.projectPath) : '';
|
|
689
|
+
const payload = await requestJson('/api/ai-hub/projects' + query).catch(() => null);
|
|
690
|
+
state.taskPaneProjects = payload && Array.isArray(payload.projects) ? payload.projects : [];
|
|
691
|
+
renderTaskPaneLauncher();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function taskPaneSwitchProject(folderPath) {
|
|
695
|
+
if (!folderPath || tfCanonicalProjectPath(folderPath) === tfCanonicalProjectPath(state.projectPath)) return;
|
|
696
|
+
const priorEmployee = document.getElementById('task-pane-employee-select')?.value || state.selectedEmployeeId;
|
|
697
|
+
await loadBootstrap(folderPath, new URLSearchParams(window.location.search).get('docUrl') || '', { preferCache: true });
|
|
698
|
+
await hydrateConversationsFromServer();
|
|
699
|
+
const latestEmployee = document.getElementById('task-pane-employee-select')?.value || priorEmployee;
|
|
700
|
+
if (latestEmployee && hubConfiguredAgents().some((agent) => agent.id === latestEmployee)) {
|
|
701
|
+
state.selectedEmployeeId = latestEmployee;
|
|
702
|
+
}
|
|
703
|
+
await refreshTaskPaneProjects();
|
|
704
|
+
renderRail();
|
|
705
|
+
renderActive();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function taskPaneStartSelectedJob() {
|
|
709
|
+
const projectSelect = document.getElementById('task-pane-project-select');
|
|
710
|
+
const employeeSelect = document.getElementById('task-pane-employee-select');
|
|
711
|
+
const jobSelect = document.getElementById('task-pane-job-select');
|
|
712
|
+
const instructions = document.getElementById('task-pane-instructions');
|
|
713
|
+
const start = document.getElementById('task-pane-start-job');
|
|
714
|
+
const statusEl = document.getElementById('task-pane-start-status');
|
|
715
|
+
const selectedProject = projectSelect && projectSelect.value;
|
|
716
|
+
const requestedEmployeeId = employeeSelect && employeeSelect.value;
|
|
717
|
+
if (selectedProject && tfCanonicalProjectPath(selectedProject) !== tfCanonicalProjectPath(state.projectPath)) {
|
|
718
|
+
await taskPaneSwitchProject(selectedProject);
|
|
719
|
+
const updatedEmployeeSelect = document.getElementById('task-pane-employee-select');
|
|
720
|
+
if (updatedEmployeeSelect && requestedEmployeeId) updatedEmployeeSelect.value = requestedEmployeeId;
|
|
721
|
+
}
|
|
722
|
+
const jobs = taskPaneJobEntries();
|
|
723
|
+
const job = jobs.find((entry) => entry.id === (jobSelect && jobSelect.value));
|
|
724
|
+
const employeeId = requestedEmployeeId || (employeeSelect && employeeSelect.value);
|
|
725
|
+
if (!job || !employeeId) return;
|
|
726
|
+
if (start) start.disabled = true;
|
|
727
|
+
if (statusEl) statusEl.textContent = 'Starting...';
|
|
728
|
+
try {
|
|
729
|
+
const result = await startRun(job, instructions ? instructions.value : '', employeeId, undefined, 'projects');
|
|
730
|
+
if (result && result.ok) {
|
|
731
|
+
if (statusEl) statusEl.textContent = 'Started.';
|
|
732
|
+
} else if (statusEl) {
|
|
733
|
+
statusEl.textContent = (result && result.error) || 'Could not start.';
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (statusEl) statusEl.textContent = 'Could not start.';
|
|
737
|
+
console.warn('[ai-hub] task-pane start failed:', error);
|
|
738
|
+
} finally {
|
|
739
|
+
renderTaskPaneLauncher();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
523
743
|
// ---------------------------------------------------------------------------
|
|
524
744
|
// Concept popover population (drives the welcome-line "see ... jobs" lists)
|
|
525
745
|
// ---------------------------------------------------------------------------
|
|
@@ -1593,6 +1813,128 @@ function aggregateDotClass(dots) {
|
|
|
1593
1813
|
return 'grey';
|
|
1594
1814
|
}
|
|
1595
1815
|
|
|
1816
|
+
function closeEmployeeManageMenu() {
|
|
1817
|
+
const existing = document.querySelector('.emp-manage-pop');
|
|
1818
|
+
if (existing) existing.remove();
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
async function refreshCustomEmployeeSurfaces() {
|
|
1822
|
+
const projectPath = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.projectPath) || state.projectPath || '';
|
|
1823
|
+
if (typeof loadBootstrap === 'function') {
|
|
1824
|
+
await loadBootstrap(projectPath);
|
|
1825
|
+
} else {
|
|
1826
|
+
if (typeof renderRail === 'function') renderRail();
|
|
1827
|
+
if (typeof tfRenderTree === 'function') tfRenderTree();
|
|
1828
|
+
if (typeof renderActive === 'function') renderActive();
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function openEmployeeManageMenu(group, anchorEl, detailsEl) {
|
|
1833
|
+
closeEmployeeManageMenu();
|
|
1834
|
+
const pop = document.createElement('div');
|
|
1835
|
+
pop.className = 'emp-manage-pop';
|
|
1836
|
+
pop.dataset.employeeKey = group.key;
|
|
1837
|
+
|
|
1838
|
+
const edit = document.createElement('button');
|
|
1839
|
+
edit.type = 'button';
|
|
1840
|
+
edit.className = 'emp-manage-edit';
|
|
1841
|
+
edit.textContent = 'Edit';
|
|
1842
|
+
edit.addEventListener('click', (e) => {
|
|
1843
|
+
e.preventDefault();
|
|
1844
|
+
e.stopPropagation();
|
|
1845
|
+
closeEmployeeManageMenu();
|
|
1846
|
+
const persona = typeof tfPersonaByKey === 'function' ? tfPersonaByKey(group.key) : null;
|
|
1847
|
+
if (persona) openInlineCreateEmployee({ mode: 'edit', employee: persona });
|
|
1848
|
+
});
|
|
1849
|
+
pop.appendChild(edit);
|
|
1850
|
+
|
|
1851
|
+
const del = document.createElement('button');
|
|
1852
|
+
del.type = 'button';
|
|
1853
|
+
del.className = 'emp-manage-delete';
|
|
1854
|
+
del.textContent = 'Delete';
|
|
1855
|
+
del.addEventListener('click', (e) => {
|
|
1856
|
+
e.preventDefault();
|
|
1857
|
+
e.stopPropagation();
|
|
1858
|
+
closeEmployeeManageMenu();
|
|
1859
|
+
showCustomEmployeeDeleteConfirm(group, detailsEl);
|
|
1860
|
+
});
|
|
1861
|
+
pop.appendChild(del);
|
|
1862
|
+
|
|
1863
|
+
const dismiss = (e) => {
|
|
1864
|
+
if (!pop.contains(e.target) && e.target !== anchorEl) {
|
|
1865
|
+
closeEmployeeManageMenu();
|
|
1866
|
+
document.removeEventListener('click', dismiss, true);
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
setTimeout(() => document.addEventListener('click', dismiss, true), 0);
|
|
1870
|
+
|
|
1871
|
+
document.body.appendChild(pop);
|
|
1872
|
+
if (anchorEl) {
|
|
1873
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
1874
|
+
pop.style.position = 'fixed';
|
|
1875
|
+
pop.style.top = (rect.bottom + 4) + 'px';
|
|
1876
|
+
pop.style.left = Math.max(8, rect.right - 112) + 'px';
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function showCustomEmployeeDeleteConfirm(group, detailsEl) {
|
|
1881
|
+
if (!detailsEl) return;
|
|
1882
|
+
const existing = detailsEl.querySelector('.emp-del-confirm');
|
|
1883
|
+
if (existing) {
|
|
1884
|
+
existing.remove();
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
document.querySelectorAll('.emp-del-confirm').forEach((el) => el.remove());
|
|
1888
|
+
detailsEl.open = true;
|
|
1889
|
+
const confirm = document.createElement('div');
|
|
1890
|
+
confirm.className = 'del-confirm emp-del-confirm';
|
|
1891
|
+
confirm.innerHTML = '<div>Delete ' + tfEscape(group.label) + '?</div>' +
|
|
1892
|
+
'<div class="dc-detail">The employee definition leaves the active roster. Prior conversations and artifacts remain.</div>';
|
|
1893
|
+
const row = document.createElement('div');
|
|
1894
|
+
row.className = 'dc-row';
|
|
1895
|
+
const cancel = document.createElement('button');
|
|
1896
|
+
cancel.type = 'button';
|
|
1897
|
+
cancel.className = 'dc-cancel';
|
|
1898
|
+
cancel.textContent = 'Cancel';
|
|
1899
|
+
cancel.addEventListener('click', (e) => {
|
|
1900
|
+
e.preventDefault();
|
|
1901
|
+
e.stopPropagation();
|
|
1902
|
+
confirm.remove();
|
|
1903
|
+
});
|
|
1904
|
+
const del = document.createElement('button');
|
|
1905
|
+
del.type = 'button';
|
|
1906
|
+
del.className = 'dc-del';
|
|
1907
|
+
del.textContent = 'Delete employee';
|
|
1908
|
+
const detail = confirm.querySelector('.dc-detail');
|
|
1909
|
+
del.addEventListener('click', async (e) => {
|
|
1910
|
+
e.preventDefault();
|
|
1911
|
+
e.stopPropagation();
|
|
1912
|
+
del.disabled = true;
|
|
1913
|
+
del.textContent = 'Deleting...';
|
|
1914
|
+
try {
|
|
1915
|
+
const projectPath = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.projectPath) || state.projectPath || '';
|
|
1916
|
+
const resp = await fetch('/api/ai-hub/custom-employees/' + encodeURIComponent(group.key) + '?projectPath=' + encodeURIComponent(projectPath), {
|
|
1917
|
+
method: 'DELETE',
|
|
1918
|
+
});
|
|
1919
|
+
if (!resp.ok && resp.status !== 404) throw new Error('Server error: ' + resp.status);
|
|
1920
|
+
if (state.cpPersonaOverride === group.key && typeof closePalette === 'function') closePalette();
|
|
1921
|
+
const overlay = document.querySelector('.ipe-overlay');
|
|
1922
|
+
if (overlay) overlay.remove();
|
|
1923
|
+
await refreshCustomEmployeeSurfaces();
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
del.disabled = false;
|
|
1926
|
+
del.textContent = 'Delete employee';
|
|
1927
|
+
if (detail) detail.textContent = 'Could not delete employee. Try again.';
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
row.appendChild(cancel);
|
|
1931
|
+
row.appendChild(del);
|
|
1932
|
+
confirm.appendChild(row);
|
|
1933
|
+
const summary = detailsEl.querySelector('summary');
|
|
1934
|
+
if (summary && summary.nextSibling) detailsEl.insertBefore(confirm, summary.nextSibling);
|
|
1935
|
+
else detailsEl.appendChild(confirm);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1596
1938
|
// ---------------------------------------------------------------------------
|
|
1597
1939
|
// Rail rendering
|
|
1598
1940
|
// ---------------------------------------------------------------------------
|
|
@@ -1919,28 +2261,53 @@ function renderRail() {
|
|
|
1919
2261
|
const count = document.createElement('span');
|
|
1920
2262
|
count.className = 'conv-employee-tab-count';
|
|
1921
2263
|
count.textContent = String(group.conversations.length);
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
addBtn
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2264
|
+
const groupPersona = getConversationPersona(group.sample);
|
|
2265
|
+
const groupIsRemoved = isRemovedCustomPersona(groupPersona);
|
|
2266
|
+
const groupIsActiveCustom = group.key.startsWith('custom:') && isActiveCustomPersona(groupPersona);
|
|
2267
|
+
let addBtn = null;
|
|
2268
|
+
if (!groupIsRemoved) {
|
|
2269
|
+
addBtn = document.createElement('button');
|
|
2270
|
+
addBtn.type = 'button';
|
|
2271
|
+
addBtn.className = 'conv-employee-add';
|
|
2272
|
+
addBtn.textContent = '+';
|
|
2273
|
+
addBtn.title = 'Assign a job to ' + group.label;
|
|
2274
|
+
addBtn.setAttribute('aria-label', 'Assign a job to ' + group.label);
|
|
2275
|
+
addBtn.addEventListener('click', (e) => {
|
|
2276
|
+
e.preventDefault();
|
|
2277
|
+
e.stopPropagation();
|
|
2278
|
+
// Issue #945: custom employees open the standard palette pre-filtered to their assigned jobs.
|
|
2279
|
+
// prefixSearch uses the display name so the user sees /nova, not /custom:nova.
|
|
2280
|
+
// personaKey is passed separately so renderCpRows can look up the jobIds correctly.
|
|
2281
|
+
if (group.key.startsWith('custom:')) {
|
|
2282
|
+
const displaySlug = group.label ? group.label.toLowerCase().replace(/\s+/g, '-') : group.key.replace('custom:', '');
|
|
2283
|
+
openPalette({ prefixSearch: '/' + displaySlug, personaKey: group.key });
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
const personaKey = group.sample && group.sample.personaKey ? group.key : null;
|
|
2287
|
+
openPalette(personaKey ? { employeeId: state.bootstrap && state.bootstrap.preferences ? state.bootstrap.preferences.employeeId : 'claude', prefixSearch: '/' + personaKey } : {});
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
let manageBtn = null;
|
|
2291
|
+
if (groupIsActiveCustom) {
|
|
2292
|
+
manageBtn = document.createElement('button');
|
|
2293
|
+
manageBtn.type = 'button';
|
|
2294
|
+
manageBtn.className = 'conv-employee-manage';
|
|
2295
|
+
manageBtn.textContent = '...';
|
|
2296
|
+
manageBtn.title = 'Manage ' + group.label;
|
|
2297
|
+
manageBtn.setAttribute('aria-label', 'Manage ' + group.label);
|
|
2298
|
+
manageBtn.addEventListener('click', (e) => {
|
|
2299
|
+
e.preventDefault();
|
|
2300
|
+
e.stopPropagation();
|
|
2301
|
+
openEmployeeManageMenu(group, manageBtn, details);
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
if (groupIsRemoved) {
|
|
2305
|
+
details.classList.add('conv-employee-group--removed');
|
|
2306
|
+
const removed = document.createElement('span');
|
|
2307
|
+
removed.className = 'emp-removed-pill';
|
|
2308
|
+
removed.textContent = 'Removed';
|
|
2309
|
+
label.appendChild(removed);
|
|
2310
|
+
} else if (groupIsActiveCustom) {
|
|
1944
2311
|
const badge = document.createElement('span');
|
|
1945
2312
|
badge.className = 'taught-badge';
|
|
1946
2313
|
badge.textContent = 'Personalized';
|
|
@@ -1960,7 +2327,8 @@ function renderRail() {
|
|
|
1960
2327
|
}
|
|
1961
2328
|
summary.appendChild(avatar);
|
|
1962
2329
|
summary.appendChild(copy);
|
|
1963
|
-
summary.appendChild(addBtn);
|
|
2330
|
+
if (addBtn) summary.appendChild(addBtn);
|
|
2331
|
+
if (manageBtn) summary.appendChild(manageBtn);
|
|
1964
2332
|
summary.appendChild(count);
|
|
1965
2333
|
details.appendChild(summary);
|
|
1966
2334
|
if (group.conversations.length > 0) {
|
|
@@ -2121,10 +2489,57 @@ function personaMap() {
|
|
|
2121
2489
|
return map;
|
|
2122
2490
|
}
|
|
2123
2491
|
|
|
2492
|
+
function customEmployeeNameFromKey(key) {
|
|
2493
|
+
const slug = String(key || '').replace(/^custom:/, '');
|
|
2494
|
+
const words = slug.split(/[-_]+/).filter(Boolean);
|
|
2495
|
+
const label = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
2496
|
+
return label || 'Employee';
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function conversationPersonaSnapshot(conv) {
|
|
2500
|
+
const snap = conv && conv.personaSnapshot;
|
|
2501
|
+
if (!snap || typeof snap !== 'object') return null;
|
|
2502
|
+
return {
|
|
2503
|
+
displayName: typeof snap.displayName === 'string' && snap.displayName.trim()
|
|
2504
|
+
? snap.displayName.trim()
|
|
2505
|
+
: '',
|
|
2506
|
+
role: typeof snap.role === 'string' && snap.role.trim()
|
|
2507
|
+
? snap.role.trim()
|
|
2508
|
+
: '',
|
|
2509
|
+
avatarUrl: typeof snap.avatarUrl === 'string' && snap.avatarUrl.trim()
|
|
2510
|
+
? snap.avatarUrl.trim()
|
|
2511
|
+
: '',
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function removedCustomPersonaForConversation(conv) {
|
|
2516
|
+
const key = conv && conv.personaKey;
|
|
2517
|
+
if (!key || !key.startsWith('custom:')) return null;
|
|
2518
|
+
const snapshot = conversationPersonaSnapshot(conv);
|
|
2519
|
+
const name = (snapshot && snapshot.displayName) || customEmployeeNameFromKey(key);
|
|
2520
|
+
return {
|
|
2521
|
+
key,
|
|
2522
|
+
displayName: 'Removed employee: ' + name,
|
|
2523
|
+
role: (snapshot && snapshot.role) || 'Historical employee',
|
|
2524
|
+
avatarUrl: (snapshot && snapshot.avatarUrl) || '',
|
|
2525
|
+
origin: 'custom',
|
|
2526
|
+
jobIds: [],
|
|
2527
|
+
removed: true,
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function isRemovedCustomPersona(persona) {
|
|
2532
|
+
return !!(persona && persona.removed);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function isActiveCustomPersona(persona) {
|
|
2536
|
+
return !!(persona && !persona.removed && (persona.origin === 'custom' || String(persona.key || '').startsWith('custom:')));
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2124
2539
|
function getConversationPersona(conv) {
|
|
2125
2540
|
if (!conv || !conv.personaKey) return null;
|
|
2126
2541
|
if (conv.personaKey === GENERIC_WORKER_PERSONA_KEY) return GENERIC_WORKER_PERSONA;
|
|
2127
|
-
return personaMap().get(conv.personaKey) ||
|
|
2542
|
+
return personaMap().get(conv.personaKey) || removedCustomPersonaForConversation(conv);
|
|
2128
2543
|
}
|
|
2129
2544
|
|
|
2130
2545
|
// Unprotected jobs default to mandy so unassigned work surfaces
|
|
@@ -6779,6 +7194,17 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
6779
7194
|
// Issue #442: read the A/B toggle state from the modal before it closes.
|
|
6780
7195
|
const abToggle = document.getElementById('ab-toggle');
|
|
6781
7196
|
const isAB = !isFreeform && abToggle && abToggle.checked;
|
|
7197
|
+
const assignedPersonaKey = assignedPersonaKeyForJob(job);
|
|
7198
|
+
const assignedPersona = assignedPersonaKey && assignedPersonaKey.startsWith('custom:')
|
|
7199
|
+
? (typeof tfPersonaByKey === 'function' ? tfPersonaByKey(assignedPersonaKey) : null)
|
|
7200
|
+
: null;
|
|
7201
|
+
const personaSnapshot = assignedPersona
|
|
7202
|
+
? {
|
|
7203
|
+
displayName: assignedPersona.displayName || assignedPersonaKey,
|
|
7204
|
+
role: assignedPersona.role || '',
|
|
7205
|
+
avatarUrl: assignedPersona.avatarUrl || '',
|
|
7206
|
+
}
|
|
7207
|
+
: null;
|
|
6782
7208
|
|
|
6783
7209
|
const conv = {
|
|
6784
7210
|
id: preassignedConvId || newConversationId(),
|
|
@@ -6792,7 +7218,8 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
6792
7218
|
baseHostId: baseHostIdForAgent(employeeId),
|
|
6793
7219
|
// Structured jobs without a protected employee default to MANdy; freeform
|
|
6794
7220
|
// described goals remain employee-less until promotion assigns ownership.
|
|
6795
|
-
personaKey:
|
|
7221
|
+
personaKey: assignedPersonaKey,
|
|
7222
|
+
...(personaSnapshot ? { personaSnapshot } : {}),
|
|
6796
7223
|
runId: null,
|
|
6797
7224
|
sessionId: null,
|
|
6798
7225
|
status: 'running',
|
|
@@ -7071,6 +7498,9 @@ function foldRunIntoConversation(conv, run) {
|
|
|
7071
7498
|
conv.title = conversationTitle(conv);
|
|
7072
7499
|
// Track session for resumption.
|
|
7073
7500
|
if (run.sessionId) conv.sessionId = run.sessionId;
|
|
7501
|
+
if (run.hostSessions && typeof run.hostSessions === 'object') {
|
|
7502
|
+
conv.hostSessions = { ...(conv.hostSessions || {}), ...run.hostSessions };
|
|
7503
|
+
}
|
|
7074
7504
|
const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
|
|
7075
7505
|
if (runHandoff) {
|
|
7076
7506
|
conv.reviewHandoff = runHandoff;
|
|
@@ -8233,13 +8663,13 @@ function wireEvents() {
|
|
|
8233
8663
|
}
|
|
8234
8664
|
|
|
8235
8665
|
// Task-pane and extension surfaces: skip welcome/onboarding, show active job
|
|
8236
|
-
//
|
|
8666
|
+
// plus a compact project/job launcher. Layout is compact (full shell hidden via CSS).
|
|
8237
8667
|
if (surface === 'task-pane' || surface === 'extension') {
|
|
8668
|
+
await refreshTaskPaneProjects();
|
|
8669
|
+
renderTaskPaneLauncher();
|
|
8238
8670
|
const conv = activeConversation();
|
|
8239
8671
|
if (conv && conv.projectPath === state.projectPath) {
|
|
8240
8672
|
if (convNeedsPolling(conv)) startPolling();
|
|
8241
|
-
} else {
|
|
8242
|
-
openModal();
|
|
8243
8673
|
}
|
|
8244
8674
|
return;
|
|
8245
8675
|
}
|
|
@@ -9028,9 +9458,8 @@ function tfRenderOverview() {
|
|
|
9028
9458
|
}
|
|
9029
9459
|
card.appendChild(team);
|
|
9030
9460
|
card.addEventListener('click', () => tfSelectProjectView('workspace', proj.id));
|
|
9031
|
-
// #
|
|
9032
|
-
|
|
9033
|
-
if (tf.projects.length > 1) tfAttachProjectRemove(card, proj);
|
|
9461
|
+
// Issue #1044: remove button shown on every project card — the Hub can be empty.
|
|
9462
|
+
tfAttachProjectRemove(card, proj);
|
|
9034
9463
|
grid.appendChild(card);
|
|
9035
9464
|
}
|
|
9036
9465
|
const addCard = document.createElement('div');
|
|
@@ -9200,17 +9629,19 @@ async function tfRefreshProjectsFromServerInBackground() {
|
|
|
9200
9629
|
|
|
9201
9630
|
async function tfRemoveProject(proj) {
|
|
9202
9631
|
const removedCanonical = tfCanonicalProjectPath(proj.folderPath || '');
|
|
9203
|
-
//
|
|
9204
|
-
// the
|
|
9632
|
+
// Issue #1044: when other projects exist, switch away from the active one before
|
|
9633
|
+
// deleting it (the server returns 409 if we don't). When it is the last project,
|
|
9634
|
+
// skip the switch — the server now permits the delete and the Hub shows empty state.
|
|
9205
9635
|
if (removedCanonical === tfCanonicalProjectPath(state.projectPath)) {
|
|
9206
9636
|
const next = tf.projects.find((p) => p.id !== proj.id && tfCanonicalProjectPath(p.folderPath || '') !== removedCanonical);
|
|
9207
|
-
if (
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9637
|
+
if (next) {
|
|
9638
|
+
try {
|
|
9639
|
+
await tfSwitchProjectFolder(next.folderPath);
|
|
9640
|
+
tf.activeProjectId = next.id;
|
|
9641
|
+
} catch (e) {
|
|
9642
|
+
showStatus('Could not switch to another project first; nothing was removed.', true);
|
|
9643
|
+
return;
|
|
9644
|
+
}
|
|
9214
9645
|
}
|
|
9215
9646
|
}
|
|
9216
9647
|
|
|
@@ -13942,10 +14373,16 @@ function openCustomEmployeePicker(employeeKey, persona, anchorEl) {
|
|
|
13942
14373
|
}
|
|
13943
14374
|
}
|
|
13944
14375
|
|
|
13945
|
-
// Renders the inline create card for a
|
|
13946
|
-
function openInlineCreateEmployee() {
|
|
14376
|
+
// Renders the inline create/edit card for a custom employee.
|
|
14377
|
+
function openInlineCreateEmployee(options) {
|
|
14378
|
+
const opts = options || {};
|
|
14379
|
+
const editEmployee = opts.mode === 'edit' ? opts.employee : null;
|
|
14380
|
+
const isEdit = !!(editEmployee && editEmployee.key);
|
|
13947
14381
|
const existingOverlay = document.querySelector('.ipe-overlay');
|
|
13948
|
-
if (existingOverlay) {
|
|
14382
|
+
if (existingOverlay) {
|
|
14383
|
+
existingOverlay.remove();
|
|
14384
|
+
if (!isEdit) return;
|
|
14385
|
+
}
|
|
13949
14386
|
const projectPath = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.projectPath) || state.projectPath || '';
|
|
13950
14387
|
const jobs = (state.bootstrap && state.bootstrap.jobs) ? state.bootstrap.jobs : [];
|
|
13951
14388
|
|
|
@@ -13961,7 +14398,7 @@ function openInlineCreateEmployee() {
|
|
|
13961
14398
|
const hdr = document.createElement('div');
|
|
13962
14399
|
hdr.className = 'modal-hdr';
|
|
13963
14400
|
const hdrTitle = document.createElement('h2');
|
|
13964
|
-
hdrTitle.textContent = 'New employee';
|
|
14401
|
+
hdrTitle.textContent = isEdit ? 'Edit employee' : 'New employee';
|
|
13965
14402
|
const closeBtn = document.createElement('button');
|
|
13966
14403
|
closeBtn.type = 'button';
|
|
13967
14404
|
closeBtn.className = 'modal-close';
|
|
@@ -13988,6 +14425,7 @@ function openInlineCreateEmployee() {
|
|
|
13988
14425
|
nameInput.placeholder = 'e.g. Nova';
|
|
13989
14426
|
nameInput.setAttribute('aria-label', 'Employee name');
|
|
13990
14427
|
nameInput.autocomplete = 'off';
|
|
14428
|
+
if (isEdit) nameInput.value = editEmployee.displayName || '';
|
|
13991
14429
|
nameField.appendChild(nameLabel);
|
|
13992
14430
|
nameField.appendChild(nameInput);
|
|
13993
14431
|
body.appendChild(nameField);
|
|
@@ -14003,12 +14441,13 @@ function openInlineCreateEmployee() {
|
|
|
14003
14441
|
roleInput.name = 'role';
|
|
14004
14442
|
roleInput.placeholder = 'e.g. AI Data Analyst';
|
|
14005
14443
|
roleInput.autocomplete = 'off';
|
|
14444
|
+
if (isEdit) roleInput.value = editEmployee.role || '';
|
|
14006
14445
|
roleField.appendChild(roleLabel);
|
|
14007
14446
|
roleField.appendChild(roleInput);
|
|
14008
14447
|
body.appendChild(roleField);
|
|
14009
14448
|
|
|
14010
14449
|
// Job picker — searchable list matching the command palette style.
|
|
14011
|
-
const selectedJobIds = new Set();
|
|
14450
|
+
const selectedJobIds = new Set(isEdit && Array.isArray(editEmployee.jobIds) ? editEmployee.jobIds : []);
|
|
14012
14451
|
const jobsField = document.createElement('div');
|
|
14013
14452
|
jobsField.className = 'ipe-card-field';
|
|
14014
14453
|
const jobsLabel = document.createElement('div');
|
|
@@ -14098,7 +14537,7 @@ function openInlineCreateEmployee() {
|
|
|
14098
14537
|
const createBtn = document.createElement('button');
|
|
14099
14538
|
createBtn.type = 'button';
|
|
14100
14539
|
createBtn.className = 'ipe-btn-create';
|
|
14101
|
-
createBtn.textContent = 'Create';
|
|
14540
|
+
createBtn.textContent = isEdit ? 'Save' : 'Create';
|
|
14102
14541
|
createBtn.addEventListener('click', async () => {
|
|
14103
14542
|
const displayName = nameInput.value.trim();
|
|
14104
14543
|
const pickedJobIds = Array.from(selectedJobIds);
|
|
@@ -14115,7 +14554,7 @@ function openInlineCreateEmployee() {
|
|
|
14115
14554
|
}
|
|
14116
14555
|
errorEl.style.display = 'none';
|
|
14117
14556
|
createBtn.disabled = true;
|
|
14118
|
-
createBtn.textContent = 'Creating
|
|
14557
|
+
createBtn.textContent = isEdit ? 'Saving...' : 'Creating...';
|
|
14119
14558
|
try {
|
|
14120
14559
|
const body = {
|
|
14121
14560
|
projectPath,
|
|
@@ -14124,26 +14563,30 @@ function openInlineCreateEmployee() {
|
|
|
14124
14563
|
icon: { kind: 'generated', value: displayName },
|
|
14125
14564
|
jobIds: pickedJobIds,
|
|
14126
14565
|
};
|
|
14127
|
-
const resp = await fetch(
|
|
14128
|
-
|
|
14566
|
+
const resp = await fetch(isEdit
|
|
14567
|
+
? '/api/ai-hub/custom-employees/' + encodeURIComponent(editEmployee.key)
|
|
14568
|
+
: '/api/ai-hub/custom-employees', {
|
|
14569
|
+
method: isEdit ? 'PATCH' : 'POST',
|
|
14129
14570
|
headers: { 'Content-Type': 'application/json' },
|
|
14130
14571
|
body: JSON.stringify(body),
|
|
14131
14572
|
});
|
|
14132
|
-
if (!resp.ok)
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
await tfBootstrap();
|
|
14137
|
-
} else if (typeof loadBootstrap === 'function') {
|
|
14138
|
-
await loadBootstrap();
|
|
14573
|
+
if (!resp.ok) {
|
|
14574
|
+
const error = new Error('Server error: ' + resp.status);
|
|
14575
|
+
error.status = resp.status;
|
|
14576
|
+
throw error;
|
|
14139
14577
|
}
|
|
14140
|
-
|
|
14141
|
-
|
|
14578
|
+
overlay.remove();
|
|
14579
|
+
await refreshCustomEmployeeSurfaces();
|
|
14142
14580
|
} catch (err) {
|
|
14143
14581
|
createBtn.disabled = false;
|
|
14144
|
-
createBtn.textContent = 'Create';
|
|
14145
|
-
errorEl.textContent =
|
|
14582
|
+
createBtn.textContent = isEdit ? 'Save' : 'Create';
|
|
14583
|
+
errorEl.textContent = isEdit
|
|
14584
|
+
? (err && err.status === 404 ? 'This employee no longer exists.' : 'Could not save employee. Try again.')
|
|
14585
|
+
: 'Could not create employee. Try again.';
|
|
14146
14586
|
errorEl.style.display = '';
|
|
14587
|
+
if (isEdit && err && err.status === 404) {
|
|
14588
|
+
await refreshCustomEmployeeSurfaces().catch(() => undefined);
|
|
14589
|
+
}
|
|
14147
14590
|
}
|
|
14148
14591
|
});
|
|
14149
14592
|
actions.appendChild(cancelBtn);
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -596,6 +596,72 @@ img.conv-employee-avatar {
|
|
|
596
596
|
background: var(--accent-soft);
|
|
597
597
|
border-color: rgba(0,113,227,.28);
|
|
598
598
|
}
|
|
599
|
+
.conv-employee-manage {
|
|
600
|
+
width: 20px;
|
|
601
|
+
height: 20px;
|
|
602
|
+
border-radius: 6px;
|
|
603
|
+
border: 1px solid var(--line);
|
|
604
|
+
background: var(--surface);
|
|
605
|
+
color: var(--muted);
|
|
606
|
+
font-size: 13px;
|
|
607
|
+
line-height: 14px;
|
|
608
|
+
font-weight: 700;
|
|
609
|
+
text-align: center;
|
|
610
|
+
cursor: pointer;
|
|
611
|
+
flex-shrink: 0;
|
|
612
|
+
padding: 0 0 3px;
|
|
613
|
+
}
|
|
614
|
+
.conv-employee-manage:hover,
|
|
615
|
+
.conv-employee-manage:focus-visible {
|
|
616
|
+
background: var(--accent-soft);
|
|
617
|
+
color: var(--accent);
|
|
618
|
+
border-color: rgba(0,113,227,.28);
|
|
619
|
+
}
|
|
620
|
+
.emp-manage-pop {
|
|
621
|
+
z-index: var(--z-dropdown-menu);
|
|
622
|
+
min-width: 112px;
|
|
623
|
+
display: grid;
|
|
624
|
+
gap: 2px;
|
|
625
|
+
padding: 5px;
|
|
626
|
+
border: 1px solid var(--line);
|
|
627
|
+
border-radius: 8px;
|
|
628
|
+
background: var(--surface);
|
|
629
|
+
box-shadow: var(--shadow-lg);
|
|
630
|
+
}
|
|
631
|
+
.emp-manage-pop button {
|
|
632
|
+
border: 0;
|
|
633
|
+
border-radius: 6px;
|
|
634
|
+
background: transparent;
|
|
635
|
+
color: var(--text);
|
|
636
|
+
font-size: 12px;
|
|
637
|
+
text-align: left;
|
|
638
|
+
padding: 6px 8px;
|
|
639
|
+
}
|
|
640
|
+
.emp-manage-pop button:hover,
|
|
641
|
+
.emp-manage-pop button:focus-visible {
|
|
642
|
+
background: var(--accent-soft);
|
|
643
|
+
outline: none;
|
|
644
|
+
}
|
|
645
|
+
.emp-manage-delete { color: var(--danger) !important; }
|
|
646
|
+
.conv-employee-group--removed .conv-employee-tab-label {
|
|
647
|
+
color: var(--muted);
|
|
648
|
+
letter-spacing: 0;
|
|
649
|
+
text-transform: none;
|
|
650
|
+
}
|
|
651
|
+
.emp-removed-pill {
|
|
652
|
+
display: inline-flex;
|
|
653
|
+
align-items: center;
|
|
654
|
+
margin-left: 6px;
|
|
655
|
+
padding: 1px 5px;
|
|
656
|
+
border-radius: 999px;
|
|
657
|
+
background: var(--warn-soft);
|
|
658
|
+
color: var(--warn);
|
|
659
|
+
font-size: 9px;
|
|
660
|
+
font-weight: 700;
|
|
661
|
+
letter-spacing: 0;
|
|
662
|
+
text-transform: uppercase;
|
|
663
|
+
}
|
|
664
|
+
.emp-del-confirm { margin-left: 28px; margin-right: 4px; }
|
|
599
665
|
/* Issue #1008: No FRAIM employee group modifier */
|
|
600
666
|
/* R4: dashed avatar placeholder — no image or persona initials */
|
|
601
667
|
.conv-employee-avatar--absence,
|
|
@@ -2804,6 +2870,81 @@ body:is([data-surface="task-pane"],[data-surface="extension"]) {
|
|
|
2804
2870
|
font-size: 13px;
|
|
2805
2871
|
overflow: auto;
|
|
2806
2872
|
}
|
|
2873
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) .hub-tabs,
|
|
2874
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) .hub-area,
|
|
2875
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) .proj-tabs {
|
|
2876
|
+
display: none !important;
|
|
2877
|
+
}
|
|
2878
|
+
.task-pane-launcher { display: none; }
|
|
2879
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) .task-pane-launcher {
|
|
2880
|
+
display: flex;
|
|
2881
|
+
flex-direction: column;
|
|
2882
|
+
gap: 10px;
|
|
2883
|
+
padding: 12px;
|
|
2884
|
+
border-bottom: 1px solid var(--border, rgba(0,0,0,0.07));
|
|
2885
|
+
background: var(--surface, #fff);
|
|
2886
|
+
}
|
|
2887
|
+
.tp-launcher-header {
|
|
2888
|
+
display: flex;
|
|
2889
|
+
flex-direction: column;
|
|
2890
|
+
gap: 2px;
|
|
2891
|
+
}
|
|
2892
|
+
.tp-launcher-title {
|
|
2893
|
+
font-size: 14px;
|
|
2894
|
+
font-weight: 650;
|
|
2895
|
+
color: var(--text, #171717);
|
|
2896
|
+
}
|
|
2897
|
+
.tp-launcher-context,
|
|
2898
|
+
.tp-launcher-status {
|
|
2899
|
+
min-height: 16px;
|
|
2900
|
+
font-size: 11px;
|
|
2901
|
+
color: var(--muted);
|
|
2902
|
+
overflow: hidden;
|
|
2903
|
+
text-overflow: ellipsis;
|
|
2904
|
+
white-space: nowrap;
|
|
2905
|
+
}
|
|
2906
|
+
.tp-launcher-field {
|
|
2907
|
+
display: flex;
|
|
2908
|
+
flex-direction: column;
|
|
2909
|
+
gap: 4px;
|
|
2910
|
+
min-width: 0;
|
|
2911
|
+
font-size: 11px;
|
|
2912
|
+
font-weight: 600;
|
|
2913
|
+
color: var(--muted);
|
|
2914
|
+
}
|
|
2915
|
+
.tp-launcher-field select,
|
|
2916
|
+
.tp-launcher-field textarea {
|
|
2917
|
+
width: 100%;
|
|
2918
|
+
min-width: 0;
|
|
2919
|
+
border: 1px solid var(--border, rgba(0,0,0,0.12));
|
|
2920
|
+
border-radius: 8px;
|
|
2921
|
+
background: var(--panel, #fff);
|
|
2922
|
+
color: var(--text, #171717);
|
|
2923
|
+
font: inherit;
|
|
2924
|
+
font-size: 12px;
|
|
2925
|
+
}
|
|
2926
|
+
.tp-launcher-field select {
|
|
2927
|
+
height: 34px;
|
|
2928
|
+
padding: 0 8px;
|
|
2929
|
+
}
|
|
2930
|
+
.tp-launcher-field textarea {
|
|
2931
|
+
min-height: 78px;
|
|
2932
|
+
resize: vertical;
|
|
2933
|
+
padding: 8px;
|
|
2934
|
+
line-height: 1.35;
|
|
2935
|
+
}
|
|
2936
|
+
.tp-launcher-actions {
|
|
2937
|
+
display: grid;
|
|
2938
|
+
grid-template-columns: 1fr auto;
|
|
2939
|
+
align-items: center;
|
|
2940
|
+
gap: 8px;
|
|
2941
|
+
}
|
|
2942
|
+
body:is([data-surface="task-pane"],[data-surface="extension"]) #task-pane-start-job {
|
|
2943
|
+
min-height: 34px;
|
|
2944
|
+
padding: 7px 12px;
|
|
2945
|
+
border-radius: 8px;
|
|
2946
|
+
font-size: 12px;
|
|
2947
|
+
}
|
|
2807
2948
|
body:is([data-surface="task-pane"],[data-surface="extension"]) .page {
|
|
2808
2949
|
padding: 10px 12px 8px;
|
|
2809
2950
|
gap: 10px;
|