fraim-framework 2.0.192 → 2.0.194
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ai-hub/hosts.js +32 -6
- package/dist/src/ai-hub/manager-turns.js +13 -0
- package/dist/src/ai-hub/preferences.js +39 -6
- package/dist/src/ai-hub/server.js +45 -24
- package/dist/src/local-mcp-server/stdio-server.js +27 -4
- package/package.json +1 -1
- package/public/ai-hub/script.js +62 -35
- package/public/ai-hub/styles.css +71 -70
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -23,6 +23,7 @@ const child_process_1 = require("child_process");
|
|
|
23
23
|
const fs_1 = __importDefault(require("fs"));
|
|
24
24
|
const os_1 = __importDefault(require("os"));
|
|
25
25
|
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const manager_turns_1 = require("./manager-turns");
|
|
26
27
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
27
28
|
const mcp_config_generator_1 = require("../cli/setup/mcp-config-generator");
|
|
28
29
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
@@ -702,11 +703,11 @@ function transformHeadlessFraimMessage(message, kind) {
|
|
|
702
703
|
if (!parsed)
|
|
703
704
|
return message;
|
|
704
705
|
if (!parsed.jobId) {
|
|
706
|
+
// Issue #732: single-source the same-job continue wording via
|
|
707
|
+
// buildSameJobContinueMessage so the server (prepareContinueMessage) and
|
|
708
|
+
// this headless transform stay in lock-step.
|
|
705
709
|
if (kind === 'continue') {
|
|
706
|
-
|
|
707
|
-
return `Continue the active FRAIM job with this manager coaching:\n\n${parsed.remainder}`;
|
|
708
|
-
}
|
|
709
|
-
return 'Continue the active FRAIM job using the current session context.';
|
|
710
|
+
return (0, manager_turns_1.buildSameJobContinueMessage)(parsed.remainder);
|
|
710
711
|
}
|
|
711
712
|
return message;
|
|
712
713
|
}
|
|
@@ -1351,6 +1352,10 @@ class FakeHostRuntime {
|
|
|
1351
1352
|
{ id: 'gemini', label: 'Gemini CLI', available: true, detail: 'Test double employee.', supportsRaw: true },
|
|
1352
1353
|
{ id: 'copilot', label: 'GitHub Copilot CLI', available: true, detail: 'Test double agent tool.', supportsRaw: true },
|
|
1353
1354
|
];
|
|
1355
|
+
// Remembered across turns like a resumed agent session: the job label from the
|
|
1356
|
+
// start turn. Issue #732 — a same-job continue no longer carries a /fraim <job>
|
|
1357
|
+
// invocation, so the fake falls back to this to stay job-aware on continue.
|
|
1358
|
+
this.lastJobLabel = null;
|
|
1354
1359
|
}
|
|
1355
1360
|
detectEmployees() {
|
|
1356
1361
|
return this.employees;
|
|
@@ -1407,9 +1412,12 @@ class FakeHostRuntime {
|
|
|
1407
1412
|
}
|
|
1408
1413
|
fakeEmployeeReply(kind, message) {
|
|
1409
1414
|
const parsed = parseFraimInvocation(message);
|
|
1410
|
-
const
|
|
1415
|
+
const parsedLabel = parsed?.jobId
|
|
1411
1416
|
? parsed.jobId.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')
|
|
1412
1417
|
: null;
|
|
1418
|
+
if (parsedLabel)
|
|
1419
|
+
this.lastJobLabel = parsedLabel;
|
|
1420
|
+
const label = parsedLabel || (kind === 'continue' ? this.lastJobLabel : null);
|
|
1413
1421
|
if (kind === 'continue') {
|
|
1414
1422
|
return label
|
|
1415
1423
|
? `Understood. I'm taking another pass with ${label}.`
|
|
@@ -1442,6 +1450,9 @@ class ScriptedHostRuntime {
|
|
|
1442
1450
|
// Tracks runDiscriminant per sessionId so consecutive emitPhase calls
|
|
1443
1451
|
// can resolve onSuccess routing without each test having to repeat it.
|
|
1444
1452
|
this.discriminantBySession = new Map();
|
|
1453
|
+
// Issue #732: capture the exact message the Hub sent on the most recent
|
|
1454
|
+
// continue turn so tests can assert whether it would re-trigger get_fraim_job.
|
|
1455
|
+
this.lastContinueMessage = null;
|
|
1445
1456
|
}
|
|
1446
1457
|
detectEmployees() {
|
|
1447
1458
|
return this.employees;
|
|
@@ -1452,7 +1463,8 @@ class ScriptedHostRuntime {
|
|
|
1452
1463
|
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
1453
1464
|
return this.spawnDouble();
|
|
1454
1465
|
}
|
|
1455
|
-
continueRun(_hostId, _projectPath, sessionId,
|
|
1466
|
+
continueRun(_hostId, _projectPath, sessionId, message, handlers) {
|
|
1467
|
+
this.lastContinueMessage = message;
|
|
1456
1468
|
this.handlersBySession.set(sessionId, handlers);
|
|
1457
1469
|
handlers.onEvent({ sessionId, raw: 'scripted-session-resume' }, 'system');
|
|
1458
1470
|
return this.spawnDouble();
|
|
@@ -1517,6 +1529,20 @@ class ScriptedHostRuntime {
|
|
|
1517
1529
|
},
|
|
1518
1530
|
}, 'stdout');
|
|
1519
1531
|
}
|
|
1532
|
+
// Test API — emit a same-job seekMentoring signal that carries BOTH the
|
|
1533
|
+
// stable jobName slug and a per-invocation UUID jobId, exactly as a real
|
|
1534
|
+
// agent does after get_fraim_job (which mints a fresh randomUUID each call).
|
|
1535
|
+
// Issue #732: the tracker must key off jobName, not the UUID jobId.
|
|
1536
|
+
emitPhaseWithJobIds(runId, phaseId, status, jobName, jobId, findingsText) {
|
|
1537
|
+
const target = this.resolveSession(runId);
|
|
1538
|
+
if (!target)
|
|
1539
|
+
return;
|
|
1540
|
+
target.handlers.onEvent({
|
|
1541
|
+
sessionId: target.sessionId,
|
|
1542
|
+
raw: `scripted-seekMentoring-ids:${phaseId}:${status}`,
|
|
1543
|
+
seekMentoring: { phaseId, phaseStatus: status, jobName, jobId, findingsText },
|
|
1544
|
+
}, 'stdout');
|
|
1545
|
+
}
|
|
1520
1546
|
// Test API — emit the structured job-load signal that get_fraim_job
|
|
1521
1547
|
// produces before the first seekMentoring phase call.
|
|
1522
1548
|
emitFraimJobLoad(runId, jobId) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.extractExplicitFraimInvocation = extractExplicitFraimInvocation;
|
|
4
4
|
exports.fraimInvocationFor = fraimInvocationFor;
|
|
5
5
|
exports.buildCommunicationStyleNote = buildCommunicationStyleNote;
|
|
6
|
+
exports.buildSameJobContinueMessage = buildSameJobContinueMessage;
|
|
6
7
|
exports.buildManagerMessage = buildManagerMessage;
|
|
7
8
|
function extractExplicitFraimInvocation(text) {
|
|
8
9
|
const raw = String(text || '');
|
|
@@ -36,6 +37,18 @@ function buildCommunicationStyleNote() {
|
|
|
36
37
|
'[How to talk to me] In your messages to me, report ONLY on the job and its outcome — what you found, what you changed, the decisions you made, blockers, and what you need from me. Do NOT narrate the FRAIM machinery: don\'t announce that you are talking to FRAIM, asking your mentor, following the process, moving between phases, or calling tools (git, playwright, etc.). I can see the raw tool activity separately if I want it. Keep your updates short and about the work, not the process.',
|
|
37
38
|
].join('\n');
|
|
38
39
|
}
|
|
40
|
+
// Issue #732: a plain continue of the SAME active job must not re-load the job.
|
|
41
|
+
// Continue turns resume the existing agent session, which already has the job
|
|
42
|
+
// loaded, so this message carries NO `/fraim <job>` invocation — the headless
|
|
43
|
+
// transform therefore does not re-instruct get_fraim_job (which is slow and
|
|
44
|
+
// mints a fresh UUID job id every call). The manager coaching still reaches the
|
|
45
|
+
// agent, which advances the workflow via seekMentoring using its resumed context.
|
|
46
|
+
function buildSameJobContinueMessage(coaching) {
|
|
47
|
+
const remainder = String(coaching || '').trim();
|
|
48
|
+
return remainder
|
|
49
|
+
? `Continue the active FRAIM job with this manager coaching:\n\n${remainder}`
|
|
50
|
+
: 'Continue the active FRAIM job using the current session context.';
|
|
51
|
+
}
|
|
39
52
|
function buildManagerMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
40
53
|
const trimmed = String(instructions || '').trim();
|
|
41
54
|
const explicit = extractExplicitFraimInvocation(trimmed);
|
|
@@ -19,6 +19,7 @@ const defaultPreferences = (projectPath) => ({
|
|
|
19
19
|
recentJobInstructions: {},
|
|
20
20
|
personaKey: null,
|
|
21
21
|
projects: normalizeAiHubProjectList([], projectPath),
|
|
22
|
+
removedProjectPaths: [],
|
|
22
23
|
});
|
|
23
24
|
function normalizeProjectPath(projectPath) {
|
|
24
25
|
return path_1.default.resolve(projectPath || process.cwd());
|
|
@@ -38,6 +39,21 @@ function projectPathExists(projectPath) {
|
|
|
38
39
|
function projectIdForPath(projectPath) {
|
|
39
40
|
return `p-${(0, crypto_1.createHash)('sha1').update(canonicalProjectPath(projectPath)).digest('base64url').slice(0, 16)}`;
|
|
40
41
|
}
|
|
42
|
+
function normalizeRemovedProjectPaths(raw, currentProjectPath) {
|
|
43
|
+
if (!Array.isArray(raw))
|
|
44
|
+
return [];
|
|
45
|
+
const currentKey = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const value of raw) {
|
|
48
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
49
|
+
continue;
|
|
50
|
+
const key = canonicalProjectPath(value);
|
|
51
|
+
if (currentKey && key === currentKey)
|
|
52
|
+
continue;
|
|
53
|
+
seen.add(key);
|
|
54
|
+
}
|
|
55
|
+
return Array.from(seen);
|
|
56
|
+
}
|
|
41
57
|
function uniqueProjectId(entry, seenIds) {
|
|
42
58
|
const fallbackId = projectIdForPath(entry.folderPath);
|
|
43
59
|
const rawId = typeof entry.id === 'string' ? entry.id.trim() : '';
|
|
@@ -86,10 +102,13 @@ function normalizeProjectEntry(raw, fallbackPath) {
|
|
|
86
102
|
function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
87
103
|
const byPath = new Map();
|
|
88
104
|
const currentKey = currentProjectPath ? normalizeProjectPath(currentProjectPath) : null;
|
|
105
|
+
const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths, currentProjectPath));
|
|
89
106
|
const add = (entry) => {
|
|
90
107
|
if (!entry)
|
|
91
108
|
return;
|
|
92
109
|
const key = normalizeProjectPath(entry.folderPath);
|
|
110
|
+
if (removedKeys.has(canonicalProjectPath(key)))
|
|
111
|
+
return;
|
|
93
112
|
if (!options.includeMissing && key !== currentKey && !projectPathExists(key))
|
|
94
113
|
return;
|
|
95
114
|
const existing = byPath.get(key);
|
|
@@ -111,6 +130,7 @@ class AiHubPreferencesStore {
|
|
|
111
130
|
}
|
|
112
131
|
try {
|
|
113
132
|
const raw = JSON.parse(fs_1.default.readFileSync(this.stateFilePath, 'utf8'));
|
|
133
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(raw.removedProjectPaths, projectPath);
|
|
114
134
|
return {
|
|
115
135
|
projectPath: raw.projectPath || projectPath,
|
|
116
136
|
employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot') ? raw.employeeId : DEFAULT_EMPLOYEE,
|
|
@@ -121,7 +141,8 @@ class AiHubPreferencesStore {
|
|
|
121
141
|
: {},
|
|
122
142
|
personaKey: typeof raw.personaKey === 'string' ? raw.personaKey : null,
|
|
123
143
|
apiKey: typeof raw.apiKey === 'string' && raw.apiKey.length > 0 ? raw.apiKey : undefined,
|
|
124
|
-
projects: normalizeAiHubProjectList(Array.isArray(raw.projects) ? raw.projects : [], projectPath),
|
|
144
|
+
projects: normalizeAiHubProjectList(Array.isArray(raw.projects) ? raw.projects : [], projectPath, { removedProjectPaths }),
|
|
145
|
+
removedProjectPaths,
|
|
125
146
|
};
|
|
126
147
|
}
|
|
127
148
|
catch {
|
|
@@ -132,18 +153,30 @@ class AiHubPreferencesStore {
|
|
|
132
153
|
fs_1.default.mkdirSync(path_1.default.dirname(this.stateFilePath), { recursive: true });
|
|
133
154
|
fs_1.default.writeFileSync(this.stateFilePath, JSON.stringify(preferences, null, 2));
|
|
134
155
|
}
|
|
135
|
-
saveProjects(projectPath, projects) {
|
|
156
|
+
saveProjects(projectPath, projects, options = {}) {
|
|
136
157
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
137
158
|
const preferences = this.load(normalizedProjectPath);
|
|
138
|
-
const
|
|
139
|
-
|
|
159
|
+
const reviveKeys = new Set(normalizeRemovedProjectPaths(options.reviveRemovedPaths, normalizedProjectPath));
|
|
160
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths, normalizedProjectPath)
|
|
161
|
+
.filter((removedPath) => !reviveKeys.has(removedPath));
|
|
162
|
+
const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
|
|
163
|
+
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
140
164
|
return nextProjects;
|
|
141
165
|
}
|
|
142
166
|
mergeProjects(projectPath, projects) {
|
|
143
167
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
144
168
|
const preferences = this.load(normalizedProjectPath);
|
|
145
|
-
const
|
|
146
|
-
|
|
169
|
+
const removedProjectPaths = normalizeRemovedProjectPaths(preferences.removedProjectPaths, normalizedProjectPath);
|
|
170
|
+
const nextProjects = normalizeAiHubProjectList([...(preferences.projects || []), ...projects], normalizedProjectPath, { removedProjectPaths });
|
|
171
|
+
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
172
|
+
return nextProjects;
|
|
173
|
+
}
|
|
174
|
+
removeProject(projectPath, projects, removedProjectPath) {
|
|
175
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
176
|
+
const preferences = this.load(normalizedProjectPath);
|
|
177
|
+
const removedProjectPaths = normalizeRemovedProjectPaths([...(preferences.removedProjectPaths || []), removedProjectPath], normalizedProjectPath);
|
|
178
|
+
const nextProjects = normalizeAiHubProjectList(projects, normalizedProjectPath, { removedProjectPaths });
|
|
179
|
+
this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
|
|
147
180
|
return nextProjects;
|
|
148
181
|
}
|
|
149
182
|
remember(preferences, jobId, instructions) {
|
|
@@ -726,12 +726,17 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
726
726
|
// Issue #347: filter cross-job pollution. The agent may call
|
|
727
727
|
// seekMentoring for jobs OTHER than the one this run is tracking
|
|
728
728
|
// (e.g., consulting `organizational-learning-synthesis` mid-run).
|
|
729
|
-
// Only apply signals whose
|
|
730
|
-
//
|
|
731
|
-
//
|
|
729
|
+
// Only apply signals whose job identity matches this run's.
|
|
730
|
+
// Issue #732: match on `jobName` — the stable job-name slug that equals
|
|
731
|
+
// run.jobId — NOT `jobId`. get_fraim_job mints a fresh randomUUID as the
|
|
732
|
+
// job id on every call and the agent echoes that UUID into
|
|
733
|
+
// seekMentoring.jobId, so a UUID never equals run.jobId. Comparing the UUID
|
|
734
|
+
// discarded every real phase signal as if it were foreign-job pollution and
|
|
735
|
+
// froze the tracker at "no phases done". The UUID cannot positively identify
|
|
736
|
+
// this run's job, so it must not participate in the match.
|
|
732
737
|
const targetJobId = run.jobId;
|
|
733
|
-
const
|
|
734
|
-
if (
|
|
738
|
+
const callJobName = signal.jobName;
|
|
739
|
+
if (callJobName && targetJobId && callJobName !== targetJobId)
|
|
735
740
|
return;
|
|
736
741
|
if (signal.reviewHandoff) {
|
|
737
742
|
const normalizedReviewHandoff = normalizeReviewHandoff(signal.reviewHandoff);
|
|
@@ -1382,10 +1387,7 @@ class AiHubServer {
|
|
|
1382
1387
|
...(preferences.projects || []),
|
|
1383
1388
|
...conversationProjects,
|
|
1384
1389
|
...extras,
|
|
1385
|
-
], normalizedProjectPath);
|
|
1386
|
-
}
|
|
1387
|
-
saveKnownProjects(projectPath, projects) {
|
|
1388
|
-
return this.preferencesStore.saveProjects(path_1.default.resolve(projectPath || this.projectPath), this.knownProjects(projectPath, projects));
|
|
1390
|
+
], normalizedProjectPath, { removedProjectPaths: preferences.removedProjectPaths || [] });
|
|
1389
1391
|
}
|
|
1390
1392
|
async bootstrapResponse(projectPath, apiKey) {
|
|
1391
1393
|
const normalizedProjectPath = path_1.default.resolve(projectPath || this.projectPath);
|
|
@@ -1865,7 +1867,10 @@ class AiHubServer {
|
|
|
1865
1867
|
return null;
|
|
1866
1868
|
}
|
|
1867
1869
|
applySeekMentoringSignalToRun(run, signal) {
|
|
1868
|
-
|
|
1870
|
+
// Issue #732: promote using the stable jobName slug, not the per-call UUID
|
|
1871
|
+
// jobId (resolveHubJob would never match a UUID, leaving a freeform run
|
|
1872
|
+
// unpromoted and then dropping its phase signals).
|
|
1873
|
+
this.maybePromoteFreeformRunToJob(run, signal.jobName || signal.jobId);
|
|
1869
1874
|
applySeekMentoringSignal(run, signal);
|
|
1870
1875
|
}
|
|
1871
1876
|
applyFraimJobSignalToRun(run, signal) {
|
|
@@ -1893,15 +1898,17 @@ class AiHubServer {
|
|
|
1893
1898
|
if (!resolvedJobId) {
|
|
1894
1899
|
throw new Error('Choose a FRAIM job before starting a run, or start with /fraim <job-id>.');
|
|
1895
1900
|
}
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
|
|
1901
|
+
// `display` is the manager conversation bubble. It keeps the user's full
|
|
1902
|
+
// coaching text with the host-specific FRAIM invocation. The agent payload
|
|
1903
|
+
// still receives Hub-only helper blocks such as `[FRAIM shared browser]`
|
|
1904
|
+
// and `[How to talk to me]`; those are omitted from the visible bubble.
|
|
1899
1905
|
// #521: the shared-browser guidance is injected HERE, at the Hub layer — never
|
|
1900
1906
|
// baked into the registry job/skill. It only appears when a shared browser is
|
|
1901
1907
|
// available (env published at boot).
|
|
1902
1908
|
const browserNote = (0, managed_browser_1.buildBrowserContextNote)(process.env.FRAIM_BROWSER_CDP_ENDPOINT, process.env.FRAIM_HUB_BASE_URL);
|
|
1903
1909
|
const styleNote = (0, manager_turns_1.buildCommunicationStyleNote)();
|
|
1904
1910
|
if (resolvedJobId === '__freeform__') {
|
|
1911
|
+
const display = (0, manager_turns_1.buildManagerMessage)(hostId, resolvedJobId, 'start', instructions);
|
|
1905
1912
|
return {
|
|
1906
1913
|
jobId: resolvedJobId,
|
|
1907
1914
|
message: (0, manager_turns_1.buildManagerMessage)(hostId, resolvedJobId, 'start', instructions) + browserNote + styleNote,
|
|
@@ -1912,6 +1919,7 @@ class AiHubServer {
|
|
|
1912
1919
|
const absoluteStubPath = resolvedJob?.stubPath
|
|
1913
1920
|
? [projectPath, resolvedJob.stubPath].join('/').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1914
1921
|
: undefined;
|
|
1922
|
+
const display = (0, manager_turns_1.buildManagerMessage)(hostId, resolvedJobId, 'start', instructions);
|
|
1915
1923
|
return {
|
|
1916
1924
|
jobId: resolvedJobId,
|
|
1917
1925
|
message: (0, manager_turns_1.buildManagerMessage)(hostId, resolvedJobId, 'start', instructions, absoluteStubPath) + browserNote + styleNote,
|
|
@@ -1926,14 +1934,27 @@ class AiHubServer {
|
|
|
1926
1934
|
// passes raw invocation syntax.
|
|
1927
1935
|
const explicit = (0, manager_turns_1.extractExplicitFraimInvocation)(instructions);
|
|
1928
1936
|
const effectiveJobId = explicit?.jobId || coachingJobId || run.jobId;
|
|
1929
|
-
// #
|
|
1930
|
-
//
|
|
1937
|
+
// #730: the manager thread shows the coaching turn with high fidelity — the
|
|
1938
|
+
// correct FRAIM command in front plus what the manager actually said — built
|
|
1939
|
+
// from the shared normalizer, exactly like the start path. With no
|
|
1940
|
+
// instructions (e.g. a plain coaching-template click) buildManagerMessage
|
|
1941
|
+
// collapses to just the invocation. The employee additionally receives the
|
|
1942
|
+
// communication-style note (and, per #732 below, a lighter same-job payload);
|
|
1943
|
+
// those operational details stay out of the visible bubble.
|
|
1931
1944
|
const userText = (explicit?.remainder || instructions || '').trim();
|
|
1932
|
-
const display = (0, manager_turns_1.
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1945
|
+
const display = (0, manager_turns_1.buildManagerMessage)(run.hostId, effectiveJobId, 'continue', instructions);
|
|
1946
|
+
// Issue #732: a plain continue of the SAME active real job must not re-load
|
|
1947
|
+
// the job via get_fraim_job on every coaching turn — the host session is
|
|
1948
|
+
// resumed with the job already in context, so re-fetching is wasted latency
|
|
1949
|
+
// and tokens (the reported hub slowness) and mints a fresh UUID job id each
|
|
1950
|
+
// time. Only load the job when the continue SWITCHES jobs (a coaching
|
|
1951
|
+
// template or an explicit /fraim <other>). Freeform/adhoc runs keep the
|
|
1952
|
+
// existing buildManagerMessage path (which already emits no invocation).
|
|
1953
|
+
const switchesJob = effectiveJobId !== run.jobId;
|
|
1954
|
+
const message = (switchesJob || run.jobId === '__freeform__')
|
|
1955
|
+
? (0, manager_turns_1.buildManagerMessage)(run.hostId, effectiveJobId, 'continue', instructions) + (0, manager_turns_1.buildCommunicationStyleNote)()
|
|
1956
|
+
: (0, manager_turns_1.buildSameJobContinueMessage)(userText) + (0, manager_turns_1.buildCommunicationStyleNote)();
|
|
1957
|
+
return { message, display };
|
|
1937
1958
|
}
|
|
1938
1959
|
async computePersonas(apiKey) {
|
|
1939
1960
|
const allBundles = listHubPersonaBundles();
|
|
@@ -2270,7 +2291,7 @@ class AiHubServer {
|
|
|
2270
2291
|
if (!Array.isArray(body.projects)) {
|
|
2271
2292
|
return res.status(400).json({ error: 'projects array required' });
|
|
2272
2293
|
}
|
|
2273
|
-
const projects = this.
|
|
2294
|
+
const projects = this.preferencesStore.saveProjects(projectPath, [...this.knownProjects(projectPath), ...body.projects], { reviveRemovedPaths: Array.isArray(body.reviveRemovedPaths) ? body.reviveRemovedPaths : [] });
|
|
2274
2295
|
return res.json({ projectPath, projects, source: 'disk' });
|
|
2275
2296
|
}
|
|
2276
2297
|
catch (error) {
|
|
@@ -2312,9 +2333,9 @@ class AiHubServer {
|
|
|
2312
2333
|
// Delete the conversation-store KEY — an empty list would still re-derive
|
|
2313
2334
|
// the project via listProjectPaths (R9).
|
|
2314
2335
|
this.conversationStore.removeProject(entry.folderPath);
|
|
2315
|
-
// Persist the filtered list
|
|
2316
|
-
//
|
|
2317
|
-
const projects = this.preferencesStore.
|
|
2336
|
+
// Persist the filtered list with a tombstone: a normal project save
|
|
2337
|
+
// still merges derived projects and cannot express removal.
|
|
2338
|
+
const projects = this.preferencesStore.removeProject(projectPath, known.filter((project) => project.id !== entry.id), entry.folderPath);
|
|
2318
2339
|
return res.json({ ok: true, projects });
|
|
2319
2340
|
});
|
|
2320
2341
|
this.app.post('/api/ai-hub/api-key', (req, res) => {
|
|
@@ -1052,6 +1052,27 @@ class FraimLocalMCPServer {
|
|
|
1052
1052
|
this.collectStrings(response.result, strings);
|
|
1053
1053
|
return strings.some(s => /\{\{proxy\.delivery\.[^}]+\}\}/.test(s));
|
|
1054
1054
|
}
|
|
1055
|
+
getDeliveryPlaceholderKeys(response) {
|
|
1056
|
+
const keys = new Set();
|
|
1057
|
+
if (!response.result)
|
|
1058
|
+
return keys;
|
|
1059
|
+
const strings = [];
|
|
1060
|
+
this.collectStrings(response.result, strings);
|
|
1061
|
+
for (const text of strings) {
|
|
1062
|
+
for (const match of text.matchAll(/\{\{proxy\.delivery\.([^}]+)\}\}/g)) {
|
|
1063
|
+
keys.add(`proxy.delivery.${match[1].trim()}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return keys;
|
|
1067
|
+
}
|
|
1068
|
+
deliveryTemplatesCoverPlaceholders(templates, response) {
|
|
1069
|
+
const requiredKeys = this.getDeliveryPlaceholderKeys(response);
|
|
1070
|
+
for (const key of requiredKeys) {
|
|
1071
|
+
if (!(key in templates))
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1055
1076
|
responseHasPotentialProviderPlaceholders(response) {
|
|
1056
1077
|
if (!response.result)
|
|
1057
1078
|
return false;
|
|
@@ -1123,15 +1144,17 @@ class FraimLocalMCPServer {
|
|
|
1123
1144
|
if (!this.responseContainsDeliveryPlaceholders(response))
|
|
1124
1145
|
return;
|
|
1125
1146
|
const engine = this.ensureEngine();
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1147
|
+
const currentTemplates = engine.loadDeliveryTemplates();
|
|
1148
|
+
// Normal proxy.delivery substitution is correct once the provider contains the
|
|
1149
|
+
// key. Older local registry caches can predate newly added delivery keys, so
|
|
1150
|
+
// treat missing required keys as stale cache and refresh from the server.
|
|
1151
|
+
if (currentTemplates && this.deliveryTemplatesCoverPlaceholders(currentTemplates, response))
|
|
1129
1152
|
return;
|
|
1130
1153
|
const filename = this.getWorkingStyle() === 'Conversation'
|
|
1131
1154
|
? 'delivery-conversation.json'
|
|
1132
1155
|
: 'delivery-pr.json';
|
|
1133
1156
|
const cached = this.readCachedTemplateFile(filename);
|
|
1134
|
-
if (cached) {
|
|
1157
|
+
if (cached && this.deliveryTemplatesCoverPlaceholders(cached, response)) {
|
|
1135
1158
|
engine.setDeliveryTemplates(cached);
|
|
1136
1159
|
return;
|
|
1137
1160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.194",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/public/ai-hub/script.js
CHANGED
|
@@ -1011,6 +1011,21 @@ function aggregateDotClass(dots) {
|
|
|
1011
1011
|
|
|
1012
1012
|
function renderRail() {
|
|
1013
1013
|
renderTeamRoster();
|
|
1014
|
+
const activeRailProjectKey = (typeof tf !== 'undefined' && tf && tf.activeProjectId)
|
|
1015
|
+
|| state.projectPath
|
|
1016
|
+
|| 'current';
|
|
1017
|
+
const employeeGroupOpenState = new Map();
|
|
1018
|
+
els['conv-list'].querySelectorAll('details.conv-employee-group[data-employee-key]').forEach((details) => {
|
|
1019
|
+
const key = details.dataset ? details.dataset.employeeKey : '';
|
|
1020
|
+
const projectKey = details.dataset ? (details.dataset.projectKey || activeRailProjectKey) : activeRailProjectKey;
|
|
1021
|
+
if (key) employeeGroupOpenState.set(projectKey + ':' + key, details.open);
|
|
1022
|
+
});
|
|
1023
|
+
const employeeGroupStateKey = (key) => activeRailProjectKey + ':' + key;
|
|
1024
|
+
const preservedEmployeeGroupOpen = (key) => (
|
|
1025
|
+
employeeGroupOpenState.has(employeeGroupStateKey(key))
|
|
1026
|
+
? employeeGroupOpenState.get(employeeGroupStateKey(key))
|
|
1027
|
+
: true
|
|
1028
|
+
);
|
|
1014
1029
|
els['conv-list'].innerHTML = '';
|
|
1015
1030
|
// R4: filter by selected persona when one is active.
|
|
1016
1031
|
// #693 R1: the "Project Updates" section was retired; project-lifecycle runs
|
|
@@ -1167,7 +1182,9 @@ function renderRail() {
|
|
|
1167
1182
|
for (const group of groups.values()) {
|
|
1168
1183
|
const details = document.createElement('details');
|
|
1169
1184
|
details.className = 'conv-employee-group';
|
|
1170
|
-
details.
|
|
1185
|
+
details.dataset.employeeKey = group.key;
|
|
1186
|
+
details.dataset.projectKey = activeRailProjectKey;
|
|
1187
|
+
details.open = preservedEmployeeGroupOpen(group.key);
|
|
1171
1188
|
const summary = document.createElement('summary');
|
|
1172
1189
|
summary.className = 'conv-employee-tab';
|
|
1173
1190
|
const avatar = buildConversationEmployeeAvatar(group.sample, 'conv-employee-avatar');
|
|
@@ -1237,7 +1254,9 @@ function renderRail() {
|
|
|
1237
1254
|
if (watercoolerConvs.length > 0) {
|
|
1238
1255
|
const wcDetails = document.createElement('details');
|
|
1239
1256
|
wcDetails.className = 'conv-employee-group conv-employee-group--adhoc';
|
|
1240
|
-
wcDetails.
|
|
1257
|
+
wcDetails.dataset.employeeKey = '__watercooler__';
|
|
1258
|
+
wcDetails.dataset.projectKey = activeRailProjectKey;
|
|
1259
|
+
wcDetails.open = preservedEmployeeGroupOpen('__watercooler__');
|
|
1241
1260
|
const wcSummary = document.createElement('summary');
|
|
1242
1261
|
wcSummary.className = 'conv-employee-tab';
|
|
1243
1262
|
// #693.8: watercooler icon (replaces the blank dashed placeholder).
|
|
@@ -2918,11 +2937,19 @@ function humanizeSlug(slug) {
|
|
|
2918
2937
|
.join(' ');
|
|
2919
2938
|
}
|
|
2920
2939
|
|
|
2921
|
-
function stripStubReference(text) {
|
|
2922
|
-
|
|
2940
|
+
function stripStubReference(text, options = {}) {
|
|
2941
|
+
const cleaned = String(text || '')
|
|
2923
2942
|
.replace(/\n?\[Job stub:[^\]]+\]/gi, '')
|
|
2924
|
-
.replace(/\[Thought:\s*true\]\s*/gi, '')
|
|
2925
|
-
|
|
2943
|
+
.replace(/\[Thought:\s*true\]\s*/gi, '');
|
|
2944
|
+
if (options.preserveWhitespace) {
|
|
2945
|
+
return cleaned.replace(/\n{3,}/g, '\n\n').trim();
|
|
2946
|
+
}
|
|
2947
|
+
return cleaned.replace(/\s+/g, ' ').trim();
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function managerBubbleText(text) {
|
|
2951
|
+
return stripHubInjectedNotes(stripStubReference(text, { preserveWhitespace: true }))
|
|
2952
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
2926
2953
|
.trim();
|
|
2927
2954
|
}
|
|
2928
2955
|
|
|
@@ -3029,8 +3056,7 @@ function appendMessageDom(role, text, conv) {
|
|
|
3029
3056
|
const content = useSurfaced ? surfaced : raw;
|
|
3030
3057
|
bubble.innerHTML = formatEmployeeText(content || (conv.status === 'completed' ? 'Done — please review.' : 'Working on it…'));
|
|
3031
3058
|
} else {
|
|
3032
|
-
|
|
3033
|
-
bubble.textContent = surfaced || text;
|
|
3059
|
+
bubble.textContent = role === 'manager' ? managerBubbleText(text) : (surfaceText(role, text, conv) || text);
|
|
3034
3060
|
}
|
|
3035
3061
|
if (role === 'manager') {
|
|
3036
3062
|
const raw = document.createElement('span');
|
|
@@ -6400,13 +6426,14 @@ function tfLoadStorage() {
|
|
|
6400
6426
|
try { tf.assignments = JSON.parse(window.localStorage.getItem(STORAGE_KEY_ASSIGNMENTS_512) || '{}'); }
|
|
6401
6427
|
catch { tf.assignments = {}; }
|
|
6402
6428
|
}
|
|
6403
|
-
function tfPersistProjects() {
|
|
6429
|
+
function tfPersistProjects(options) {
|
|
6404
6430
|
try { window.localStorage.setItem(STORAGE_KEY_PROJECTS_512, JSON.stringify(tf.projects)); } catch {}
|
|
6405
6431
|
if (!state.projectPath) return;
|
|
6432
|
+
const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
|
|
6406
6433
|
requestJson('/api/ai-hub/projects', {
|
|
6407
6434
|
method: 'PUT',
|
|
6408
6435
|
headers: { 'Content-Type': 'application/json' },
|
|
6409
|
-
body: JSON.stringify({ projectPath: state.projectPath, projects: tf.projects }),
|
|
6436
|
+
body: JSON.stringify({ projectPath: state.projectPath, projects: tf.projects, reviveRemovedPaths }),
|
|
6410
6437
|
}).catch((error) => console.warn('[512] Could not persist projects to local disk:', error));
|
|
6411
6438
|
}
|
|
6412
6439
|
function tfPersistAssignments() {
|
|
@@ -9400,32 +9427,32 @@ function tfCloseConnectedSurface() {
|
|
|
9400
9427
|
tfShowArea(returnArea);
|
|
9401
9428
|
}
|
|
9402
9429
|
|
|
9403
|
-
async function tfHandleConnectedAuthMessage(event) {
|
|
9404
|
-
const frame = document.getElementById('connected-frame');
|
|
9405
|
-
if (frame && frame.contentWindow && event.source !== frame.contentWindow) return;
|
|
9406
|
-
let remoteOrigin = '';
|
|
9407
|
-
try { remoteOrigin = new URL(tfConnectedRemoteBase()).origin; }
|
|
9430
|
+
async function tfHandleConnectedAuthMessage(event) {
|
|
9431
|
+
const frame = document.getElementById('connected-frame');
|
|
9432
|
+
if (frame && frame.contentWindow && event.source !== frame.contentWindow) return;
|
|
9433
|
+
let remoteOrigin = '';
|
|
9434
|
+
try { remoteOrigin = new URL(tfConnectedRemoteBase()).origin; }
|
|
9408
9435
|
catch { return; }
|
|
9409
9436
|
if (event.origin !== remoteOrigin) return;
|
|
9410
|
-
const msg = event.data || {};
|
|
9411
|
-
if (!msg || msg.type !== 'fraim-auth-api-key' || typeof msg.apiKey !== 'string' || !msg.apiKey) return;
|
|
9412
|
-
const previousKey = tfConnectedApiKey();
|
|
9413
|
-
await tfRememberConnectedApiKey(msg.apiKey);
|
|
9414
|
-
if (state.connectedSurface) {
|
|
9415
|
-
const nextUrl = tfConnectedSurfaceUrl(state.connectedSurface);
|
|
9416
|
-
const frameEl = document.getElementById('connected-frame');
|
|
9417
|
-
if (frameEl) {
|
|
9418
|
-
try {
|
|
9419
|
-
const current = new URL(frameEl.src || '', window.location.href);
|
|
9420
|
-
const next = new URL(nextUrl, window.location.href);
|
|
9421
|
-
if (previousKey === msg.apiKey && current.origin === next.origin && current.pathname === next.pathname) {
|
|
9422
|
-
return;
|
|
9423
|
-
}
|
|
9424
|
-
} catch {}
|
|
9425
|
-
frameEl.src = nextUrl;
|
|
9426
|
-
}
|
|
9427
|
-
}
|
|
9428
|
-
}
|
|
9437
|
+
const msg = event.data || {};
|
|
9438
|
+
if (!msg || msg.type !== 'fraim-auth-api-key' || typeof msg.apiKey !== 'string' || !msg.apiKey) return;
|
|
9439
|
+
const previousKey = tfConnectedApiKey();
|
|
9440
|
+
await tfRememberConnectedApiKey(msg.apiKey);
|
|
9441
|
+
if (state.connectedSurface) {
|
|
9442
|
+
const nextUrl = tfConnectedSurfaceUrl(state.connectedSurface);
|
|
9443
|
+
const frameEl = document.getElementById('connected-frame');
|
|
9444
|
+
if (frameEl) {
|
|
9445
|
+
try {
|
|
9446
|
+
const current = new URL(frameEl.src || '', window.location.href);
|
|
9447
|
+
const next = new URL(nextUrl, window.location.href);
|
|
9448
|
+
if (previousKey === msg.apiKey && current.origin === next.origin && current.pathname === next.pathname) {
|
|
9449
|
+
return;
|
|
9450
|
+
}
|
|
9451
|
+
} catch {}
|
|
9452
|
+
frameEl.src = nextUrl;
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9429
9456
|
|
|
9430
9457
|
async function tfRememberConnectedApiKey(apiKey) {
|
|
9431
9458
|
const changed = apiKey !== state.storedApiKey;
|
|
@@ -9808,7 +9835,7 @@ async function tfCreateProject() {
|
|
|
9808
9835
|
};
|
|
9809
9836
|
tf.projects.push(project);
|
|
9810
9837
|
tf.assignments[id] = [];
|
|
9811
|
-
tfPersistProjects();
|
|
9838
|
+
tfPersistProjects({ reviveRemovedPaths: [folderPath] });
|
|
9812
9839
|
tfPersistAssignments();
|
|
9813
9840
|
tf.activeProjectId = id;
|
|
9814
9841
|
tfCloseNewProject();
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -1503,6 +1503,7 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
|
|
|
1503
1503
|
.message.manager .bubble {
|
|
1504
1504
|
background: var(--accent);
|
|
1505
1505
|
color: #fff;
|
|
1506
|
+
white-space: pre-wrap;
|
|
1506
1507
|
}
|
|
1507
1508
|
.message.employee .bubble {
|
|
1508
1509
|
background: var(--surface);
|
|
@@ -2752,7 +2753,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2752
2753
|
.am-av { width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
|
|
2753
2754
|
.am-name { font-size: 13px; font-weight: 700; }
|
|
2754
2755
|
.am-email { font-size: 11px; color: var(--muted); }
|
|
2755
|
-
.am-item { display: flex; align-items: flex-start; gap: 10px; width: 100%; padding: 9px 16px; cursor: pointer; font-size: 13px; color: var(--text); text-align: left; text-decoration: none; border: 0; background: none; }
|
|
2756
|
+
.am-item { display: flex; align-items: flex-start; gap: 10px; width: 100%; padding: 9px 16px; cursor: pointer; font-size: 13px; color: var(--text); text-align: left; text-decoration: none; border: 0; background: none; }
|
|
2756
2757
|
.am-item:hover { background: var(--bg); }
|
|
2757
2758
|
.am-ico { font-size: 14px; width: 18px; text-align: center; flex-shrink: 0; margin-top: 1px; }
|
|
2758
2759
|
.am-label { font-weight: 600; }
|
|
@@ -2768,75 +2769,75 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2768
2769
|
.am-theme[aria-checked="true"] .am-switch-knob { transform: translateX(16px); }
|
|
2769
2770
|
|
|
2770
2771
|
/* Hub areas */
|
|
2771
|
-
.hub-area { display: none; flex: 1; overflow: hidden; }
|
|
2772
|
-
.hub-area.on { display: flex; flex-direction: column; }
|
|
2773
|
-
.hub-area-page { max-width: 860px; margin: 0 auto; padding: 28px 24px 48px; overflow-y: auto; flex: 1; /* authoritative rule — see also Round-2 override below which is now removed */ }
|
|
2774
|
-
|
|
2775
|
-
.connected-shell {
|
|
2776
|
-
display: flex;
|
|
2777
|
-
flex: 1;
|
|
2778
|
-
min-height: 0;
|
|
2779
|
-
flex-direction: column;
|
|
2780
|
-
background: var(--bg);
|
|
2781
|
-
}
|
|
2782
|
-
.connected-toolbar {
|
|
2783
|
-
display: flex;
|
|
2784
|
-
align-items: center;
|
|
2785
|
-
gap: 14px;
|
|
2786
|
-
min-height: 52px;
|
|
2787
|
-
padding: 8px 16px;
|
|
2788
|
-
background: var(--surface);
|
|
2789
|
-
border-bottom: 1px solid var(--line);
|
|
2790
|
-
flex-shrink: 0;
|
|
2791
|
-
}
|
|
2792
|
-
.connected-title-block {
|
|
2793
|
-
min-width: 0;
|
|
2794
|
-
display: flex;
|
|
2795
|
-
flex-direction: column;
|
|
2796
|
-
gap: 1px;
|
|
2797
|
-
}
|
|
2798
|
-
.connected-kicker {
|
|
2799
|
-
font-size: 10px;
|
|
2800
|
-
font-weight: 700;
|
|
2801
|
-
letter-spacing: .04em;
|
|
2802
|
-
text-transform: uppercase;
|
|
2803
|
-
color: var(--muted);
|
|
2804
|
-
}
|
|
2805
|
-
.connected-title {
|
|
2806
|
-
font-size: 14px;
|
|
2807
|
-
font-weight: 700;
|
|
2808
|
-
color: var(--text);
|
|
2809
|
-
}
|
|
2810
|
-
.connected-origin {
|
|
2811
|
-
min-width: 0;
|
|
2812
|
-
flex: 1;
|
|
2813
|
-
color: var(--muted);
|
|
2814
|
-
font-size: 12px;
|
|
2815
|
-
overflow: hidden;
|
|
2816
|
-
text-overflow: ellipsis;
|
|
2817
|
-
white-space: nowrap;
|
|
2818
|
-
}
|
|
2819
|
-
.connected-close {
|
|
2820
|
-
border: 1px solid var(--line);
|
|
2821
|
-
background: var(--surface);
|
|
2822
|
-
color: var(--text);
|
|
2823
|
-
border-radius: 8px;
|
|
2824
|
-
padding: 6px 12px;
|
|
2825
|
-
font-size: 13px;
|
|
2826
|
-
font-weight: 600;
|
|
2827
|
-
flex-shrink: 0;
|
|
2828
|
-
}
|
|
2829
|
-
.connected-close:hover { background: var(--bg); }
|
|
2830
|
-
.connected-frame {
|
|
2831
|
-
flex: 1;
|
|
2832
|
-
min-height: 0;
|
|
2833
|
-
width: 100%;
|
|
2834
|
-
border: 0;
|
|
2835
|
-
background: var(--surface);
|
|
2836
|
-
}
|
|
2837
|
-
|
|
2838
|
-
/* #521 R6: Company/Manager use the same rail + main shell as the project workspace. */
|
|
2839
|
-
.area-shell { display: flex; flex: 1; min-height: 0; }
|
|
2772
|
+
.hub-area { display: none; flex: 1; overflow: hidden; }
|
|
2773
|
+
.hub-area.on { display: flex; flex-direction: column; }
|
|
2774
|
+
.hub-area-page { max-width: 860px; margin: 0 auto; padding: 28px 24px 48px; overflow-y: auto; flex: 1; /* authoritative rule — see also Round-2 override below which is now removed */ }
|
|
2775
|
+
|
|
2776
|
+
.connected-shell {
|
|
2777
|
+
display: flex;
|
|
2778
|
+
flex: 1;
|
|
2779
|
+
min-height: 0;
|
|
2780
|
+
flex-direction: column;
|
|
2781
|
+
background: var(--bg);
|
|
2782
|
+
}
|
|
2783
|
+
.connected-toolbar {
|
|
2784
|
+
display: flex;
|
|
2785
|
+
align-items: center;
|
|
2786
|
+
gap: 14px;
|
|
2787
|
+
min-height: 52px;
|
|
2788
|
+
padding: 8px 16px;
|
|
2789
|
+
background: var(--surface);
|
|
2790
|
+
border-bottom: 1px solid var(--line);
|
|
2791
|
+
flex-shrink: 0;
|
|
2792
|
+
}
|
|
2793
|
+
.connected-title-block {
|
|
2794
|
+
min-width: 0;
|
|
2795
|
+
display: flex;
|
|
2796
|
+
flex-direction: column;
|
|
2797
|
+
gap: 1px;
|
|
2798
|
+
}
|
|
2799
|
+
.connected-kicker {
|
|
2800
|
+
font-size: 10px;
|
|
2801
|
+
font-weight: 700;
|
|
2802
|
+
letter-spacing: .04em;
|
|
2803
|
+
text-transform: uppercase;
|
|
2804
|
+
color: var(--muted);
|
|
2805
|
+
}
|
|
2806
|
+
.connected-title {
|
|
2807
|
+
font-size: 14px;
|
|
2808
|
+
font-weight: 700;
|
|
2809
|
+
color: var(--text);
|
|
2810
|
+
}
|
|
2811
|
+
.connected-origin {
|
|
2812
|
+
min-width: 0;
|
|
2813
|
+
flex: 1;
|
|
2814
|
+
color: var(--muted);
|
|
2815
|
+
font-size: 12px;
|
|
2816
|
+
overflow: hidden;
|
|
2817
|
+
text-overflow: ellipsis;
|
|
2818
|
+
white-space: nowrap;
|
|
2819
|
+
}
|
|
2820
|
+
.connected-close {
|
|
2821
|
+
border: 1px solid var(--line);
|
|
2822
|
+
background: var(--surface);
|
|
2823
|
+
color: var(--text);
|
|
2824
|
+
border-radius: 8px;
|
|
2825
|
+
padding: 6px 12px;
|
|
2826
|
+
font-size: 13px;
|
|
2827
|
+
font-weight: 600;
|
|
2828
|
+
flex-shrink: 0;
|
|
2829
|
+
}
|
|
2830
|
+
.connected-close:hover { background: var(--bg); }
|
|
2831
|
+
.connected-frame {
|
|
2832
|
+
flex: 1;
|
|
2833
|
+
min-height: 0;
|
|
2834
|
+
width: 100%;
|
|
2835
|
+
border: 0;
|
|
2836
|
+
background: var(--surface);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
/* #521 R6: Company/Manager use the same rail + main shell as the project workspace. */
|
|
2840
|
+
.area-shell { display: flex; flex: 1; min-height: 0; }
|
|
2840
2841
|
.area-rail { width: 244px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--line); overflow-y: auto; padding: 6px 0; }
|
|
2841
2842
|
.area-main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
2842
2843
|
.area-rail-head { font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding: 14px 16px 6px; }
|