fraim-hub 2.0.233 → 2.0.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ai-hub/server.js +27 -9
- package/package.json +2 -2
- package/public/ai-hub/index.html +1 -1
- package/public/ai-hub/script.js +34 -0
- package/public/ai-hub/styles.css +18 -2
|
@@ -1960,7 +1960,7 @@ class AiHubServer {
|
|
|
1960
1960
|
},
|
|
1961
1961
|
};
|
|
1962
1962
|
}
|
|
1963
|
-
if (existing.jobId && existing.jobId !== options.jobId) {
|
|
1963
|
+
if (existing.jobId && existing.jobId !== '__freeform__' && existing.jobId !== options.jobId) {
|
|
1964
1964
|
console.warn('[ai-hub] hub.conversation_continuity.rejected', {
|
|
1965
1965
|
conversationId,
|
|
1966
1966
|
existingJobId: existing.jobId,
|
|
@@ -1978,6 +1978,15 @@ class AiHubServer {
|
|
|
1978
1978
|
}
|
|
1979
1979
|
return { ok: true, continuityDecision: 'same_continuity' };
|
|
1980
1980
|
}
|
|
1981
|
+
resolveImplicitResumeConversation(options) {
|
|
1982
|
+
const candidates = this.conversationStore.loadProject(options.projectPath).conversations.filter((conversation) => {
|
|
1983
|
+
if (conversation.jobId !== options.jobId)
|
|
1984
|
+
return false;
|
|
1985
|
+
const existingAgentId = this.configuredAgentIdForConversation(conversation, options.employees);
|
|
1986
|
+
return existingAgentId === options.requestedAgent.id;
|
|
1987
|
+
});
|
|
1988
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
1989
|
+
}
|
|
1981
1990
|
isTrustedHubOrigin(req) {
|
|
1982
1991
|
const origin = req.get('origin');
|
|
1983
1992
|
if (!origin)
|
|
@@ -4061,13 +4070,18 @@ class AiHubServer {
|
|
|
4061
4070
|
throw new Error('A jobId is required to resume.');
|
|
4062
4071
|
if (!instructions)
|
|
4063
4072
|
throw new Error('Provide an instruction to continue.');
|
|
4064
|
-
const
|
|
4065
|
-
const
|
|
4073
|
+
const employees = this.hostRuntime.detectEmployees();
|
|
4074
|
+
const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(body.configuredAgentId, requestedHostId, employees);
|
|
4075
|
+
const requestedConversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
4066
4076
|
? body.conversationId.trim()
|
|
4067
4077
|
: undefined;
|
|
4068
|
-
const
|
|
4069
|
-
?
|
|
4070
|
-
:
|
|
4078
|
+
const inferredConversation = requestedConversationId
|
|
4079
|
+
? null
|
|
4080
|
+
: this.resolveImplicitResumeConversation({ projectPath, jobId, requestedAgent: configuredAgent, employees });
|
|
4081
|
+
const conversationId = requestedConversationId || inferredConversation?.id;
|
|
4082
|
+
const persistedConversation = requestedConversationId
|
|
4083
|
+
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === requestedConversationId)
|
|
4084
|
+
: inferredConversation ?? undefined;
|
|
4071
4085
|
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
4072
4086
|
const now = new Date().toISOString();
|
|
4073
4087
|
const run = {
|
|
@@ -4078,12 +4092,16 @@ class AiHubServer {
|
|
|
4078
4092
|
jobId, hostId, configuredAgentId: configuredAgent.id, configuredAgentLabel: configuredAgent.label, baseHostId: configuredAgent.baseHostId, projectPath, status: 'running', sessionId,
|
|
4079
4093
|
createdAt: now, updatedAt: now, messages: [],
|
|
4080
4094
|
events: [(0, hosts_1.createHubEvent)('system', `Resuming ${configuredAgent.label} (${hostId}) session ${sessionId} in ${projectPath}`)],
|
|
4081
|
-
|
|
4082
|
-
|
|
4095
|
+
// Only carry phase state forward when the prior run was interrupted mid-job
|
|
4096
|
+
// (status !== 'completed'). A completed conversation is a finished run;
|
|
4097
|
+
// resuming the session starts a fresh job, so the tracker must be blank.
|
|
4098
|
+
currentPhase: persistedConversation?.status !== 'completed' ? (persistedRun?.currentPhase || null) : null,
|
|
4099
|
+
phaseHistory: persistedConversation?.status !== 'completed' ? (persistedRun?.phaseHistory || []) : [],
|
|
4083
4100
|
totals: persistedRun?.totals || emptyTotals(),
|
|
4084
4101
|
lastStatusChangeAt: now,
|
|
4085
|
-
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
4102
|
+
runDiscriminant: persistedConversation?.status !== 'completed' ? (persistedRun?.runDiscriminant || undefined) : undefined,
|
|
4086
4103
|
personaKey: getHubPersonaForJob(jobId),
|
|
4104
|
+
continuityDecision: conversationId ? 'same_continuity' : 'new_conversation',
|
|
4087
4105
|
};
|
|
4088
4106
|
// Continue-turn message (FRAIM invocation for the job + instructions) plus
|
|
4089
4107
|
// the shared-browser note so the resumed agent knows about it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.235",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js"
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
"electron": "^41.2.2",
|
|
159
159
|
"electron-updater": "^6.8.9",
|
|
160
160
|
"express": "^5.2.1",
|
|
161
|
-
"fraim": "2.0.
|
|
161
|
+
"fraim": "2.0.235",
|
|
162
162
|
"mongodb": "^7.0.0",
|
|
163
163
|
"node-cron": "4.2.1",
|
|
164
164
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -398,7 +398,7 @@
|
|
|
398
398
|
<div class="conv-topline">
|
|
399
399
|
<div class="employee-identity" id="active-identity"></div>
|
|
400
400
|
<span id="active-title" class="conv-job-title"></span>
|
|
401
|
-
<button class="conversation-focus-btn" id="conversation-focus-btn" type="button" aria-label="
|
|
401
|
+
<button class="conversation-focus-btn" id="conversation-focus-btn" type="button" aria-label="Pop-out conversation" title="Pop-out conversation">↗</button>
|
|
402
402
|
<button class="run-stop-btn" id="run-stop-btn" type="button" hidden title="Ask the employee to stop and wait for you">⏹ Stop</button>
|
|
403
403
|
<div class="run-state-pill" id="run-state-pill"></div>
|
|
404
404
|
</div>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -1311,6 +1311,19 @@ function stripHubInjectedPromptBlocks(text) {
|
|
|
1311
1311
|
.trim();
|
|
1312
1312
|
}
|
|
1313
1313
|
|
|
1314
|
+
// Returns a validated http/https URL string suitable for use in an href, or null if unsafe.
|
|
1315
|
+
function _safeHref(raw) {
|
|
1316
|
+
try {
|
|
1317
|
+
const url = new URL(raw);
|
|
1318
|
+
return (url.protocol === 'https:' || url.protocol === 'http:') ? url.href : null;
|
|
1319
|
+
} catch (_) { return null; }
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Escape & and " in a URL for safe insertion into an href="..." attribute.
|
|
1323
|
+
function _escapeHref(href) {
|
|
1324
|
+
return href.replace(/&/g, '&').replace(/"/g, '"');
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1314
1327
|
// R8: render markdown subset safely. HTML is escaped first.
|
|
1315
1328
|
function formatEmployeeText(text) {
|
|
1316
1329
|
if (!text) return '';
|
|
@@ -1326,6 +1339,27 @@ function formatEmployeeText(text) {
|
|
|
1326
1339
|
s = s.replace(/```[\w]*\n?([\s\S]*?)```/g, (_, code) => `<pre><code>${code.trimEnd()}</code></pre>`);
|
|
1327
1340
|
// 3. Inline code (single backtick, non-greedy, no newlines).
|
|
1328
1341
|
s = s.replace(/`([^`\n]+)`/g, '<code>$1</code>');
|
|
1342
|
+
// 3.5. Markdown links [text](url) — only http/https hrefs allowed; link text is already HTML-escaped.
|
|
1343
|
+
// Empty-text links and non-absolute URLs fall back to plain text.
|
|
1344
|
+
s = s.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (match, linkText, rawUrl) => {
|
|
1345
|
+
const trimmed = linkText.trim();
|
|
1346
|
+
// Empty link text: render as plain text; sentinel prevents step 3.6 auto-linkify.
|
|
1347
|
+
if (!trimmed) return `\x00NOLINK\x00${rawUrl.trim()}\x00ENDNOLINK\x00`;
|
|
1348
|
+
// Reverse HTML entities step 1 introduced inside the URL before structural validation.
|
|
1349
|
+
const decodedUrl = rawUrl.trim().replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
1350
|
+
const href = _safeHref(decodedUrl);
|
|
1351
|
+
if (!href) return trimmed;
|
|
1352
|
+
return `<a href="${_escapeHref(href)}" target="_blank" rel="noopener noreferrer">${trimmed}</a>`;
|
|
1353
|
+
});
|
|
1354
|
+
// 3.6. Auto-linkify bare https:// / http:// URLs not already inside href="...".
|
|
1355
|
+
// Sentinel prefix prevents double-linkifying empty-text [](url) patterns.
|
|
1356
|
+
s = s.replace(/(?<!href=")(?<!href=')(?<!\x00NOLINK\x00)(https?:\/\/[^\s<>"'\x00]+)/g, (url) => {
|
|
1357
|
+
const href = _safeHref(url);
|
|
1358
|
+
if (!href) return url;
|
|
1359
|
+
return `<a href="${_escapeHref(href)}" target="_blank" rel="noopener noreferrer">${_escapeHref(href)}</a>`;
|
|
1360
|
+
});
|
|
1361
|
+
// Strip sentinels: emit the URL as plain text (no anchor).
|
|
1362
|
+
s = s.replace(/\x00NOLINK\x00(.*?)\x00ENDNOLINK\x00/g, (_, url) => url);
|
|
1329
1363
|
// 4. Bold (**text**).
|
|
1330
1364
|
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
1331
1365
|
// 5–7. Process line by line for lists and paragraphs.
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -804,13 +804,19 @@ img.conv-employee-avatar {
|
|
|
804
804
|
.run-stop-btn:disabled { opacity: .55; cursor: default; }
|
|
805
805
|
.conversation-focus-btn {
|
|
806
806
|
flex-shrink: 0;
|
|
807
|
-
|
|
807
|
+
display: inline-flex;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: center;
|
|
810
|
+
width: 32px;
|
|
811
|
+
height: 32px;
|
|
812
|
+
font-size: 17px;
|
|
808
813
|
font-weight: 700;
|
|
814
|
+
line-height: 1;
|
|
809
815
|
color: var(--accent-strong);
|
|
810
816
|
background: var(--accent-soft);
|
|
811
817
|
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
|
|
812
818
|
border-radius: 8px;
|
|
813
|
-
padding:
|
|
819
|
+
padding: 0;
|
|
814
820
|
cursor: pointer;
|
|
815
821
|
}
|
|
816
822
|
.conversation-focus-btn:hover {
|
|
@@ -826,6 +832,7 @@ img.conv-employee-avatar {
|
|
|
826
832
|
.conv-job-title {
|
|
827
833
|
flex: 1;
|
|
828
834
|
min-width: 0;
|
|
835
|
+
max-width: 100%;
|
|
829
836
|
font-size: 15px;
|
|
830
837
|
font-weight: 600;
|
|
831
838
|
color: var(--text);
|
|
@@ -1669,6 +1676,15 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
|
|
|
1669
1676
|
border: 1px solid var(--line);
|
|
1670
1677
|
color: var(--text);
|
|
1671
1678
|
}
|
|
1679
|
+
.message.employee .bubble a {
|
|
1680
|
+
color: var(--accent);
|
|
1681
|
+
text-decoration: underline;
|
|
1682
|
+
text-underline-offset: 2px;
|
|
1683
|
+
word-break: break-all;
|
|
1684
|
+
}
|
|
1685
|
+
.message.employee .bubble a:hover {
|
|
1686
|
+
color: var(--accent-strong);
|
|
1687
|
+
}
|
|
1672
1688
|
.typing-indicator {
|
|
1673
1689
|
animation: none;
|
|
1674
1690
|
justify-items: start;
|