fraim 2.0.137 → 2.0.138
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 +4 -4
- package/package.json +1 -1
- package/public/ai-hub/index.html +8 -1
- package/public/ai-hub/script.js +135 -14
- package/public/ai-hub/styles.css +22 -0
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -251,14 +251,14 @@ function buildStartPlan(hostId, message) {
|
|
|
251
251
|
if (hostId === 'codex') {
|
|
252
252
|
return {
|
|
253
253
|
command: executableName('codex'),
|
|
254
|
-
args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox'],
|
|
254
|
+
args: ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--model', 'gpt-4o'],
|
|
255
255
|
stdin: message,
|
|
256
256
|
};
|
|
257
257
|
}
|
|
258
258
|
if (hostId === 'gemini') {
|
|
259
259
|
return {
|
|
260
260
|
command: executableName('gemini'),
|
|
261
|
-
args: ['--yolo'],
|
|
261
|
+
args: ['--yolo', '--skip-trust'],
|
|
262
262
|
stdin: message,
|
|
263
263
|
};
|
|
264
264
|
}
|
|
@@ -272,7 +272,7 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
272
272
|
if (hostId === 'codex') {
|
|
273
273
|
return {
|
|
274
274
|
command: executableName('codex'),
|
|
275
|
-
args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', sessionId],
|
|
275
|
+
args: ['exec', 'resume', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--model', 'gpt-4o', sessionId],
|
|
276
276
|
stdin: message,
|
|
277
277
|
};
|
|
278
278
|
}
|
|
@@ -281,7 +281,7 @@ function buildContinuePlan(hostId, sessionId, message) {
|
|
|
281
281
|
// is sent as a fresh invocation. The Hub still tracks state client-side.
|
|
282
282
|
return {
|
|
283
283
|
command: executableName('gemini'),
|
|
284
|
-
args: ['--yolo'],
|
|
284
|
+
args: ['--yolo', '--skip-trust'],
|
|
285
285
|
stdin: message,
|
|
286
286
|
};
|
|
287
287
|
}
|
package/package.json
CHANGED
package/public/ai-hub/index.html
CHANGED
|
@@ -96,7 +96,13 @@
|
|
|
96
96
|
<div class="messages" id="messages"></div>
|
|
97
97
|
|
|
98
98
|
<div class="coach">
|
|
99
|
-
<div class="
|
|
99
|
+
<div class="coach-title-row">
|
|
100
|
+
<span class="section-title">Coach the employee</span>
|
|
101
|
+
<span class="active-employee-row">
|
|
102
|
+
<label for="active-employee-select" class="active-employee-label">via</label>
|
|
103
|
+
<select id="active-employee-select" class="employee-select inline"></select>
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
100
106
|
<textarea id="coach-text" placeholder="Tell the employee what to do next…"></textarea>
|
|
101
107
|
<div class="coach-actions">
|
|
102
108
|
<!-- Issue #347 R2: template picker. Hidden when the project
|
|
@@ -138,6 +144,7 @@
|
|
|
138
144
|
<span class="left" id="job-pick-status">Choose a job to continue</span>
|
|
139
145
|
<div class="right">
|
|
140
146
|
<button class="ghost" type="button" id="cancel1">Cancel</button>
|
|
147
|
+
<button class="ghost" type="button" id="freeform-btn">Just describe what you need</button>
|
|
141
148
|
<button class="send-button" type="button" id="next1" disabled>Next →</button>
|
|
142
149
|
</div>
|
|
143
150
|
</div>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -40,7 +40,8 @@ function gatherElements() {
|
|
|
40
40
|
'cancel1', 'next1', 'back2', 'start',
|
|
41
41
|
'job-search', 'job-catalog', 'job-pick-status',
|
|
42
42
|
'picked-name', 'picked-desc', 'instructions',
|
|
43
|
-
'employee-select', 'agent-install-panel',
|
|
43
|
+
'employee-select', 'agent-install-panel', 'active-employee-select',
|
|
44
|
+
'freeform-btn',
|
|
44
45
|
// Issue #347 additions: tracker, template picker, totals.
|
|
45
46
|
'tracker', 'tracker-rows', 'tracker-note',
|
|
46
47
|
'template-picker-btn', 'template-popover',
|
|
@@ -349,6 +350,9 @@ function renderActive() {
|
|
|
349
350
|
// Coaching state — only enable Send when there's text and the run is resumable.
|
|
350
351
|
syncSendButton();
|
|
351
352
|
|
|
353
|
+
// Render the active employee selector in the coach section.
|
|
354
|
+
renderActiveEmployeeSelect(conv);
|
|
355
|
+
|
|
352
356
|
// Issue #347 — tracker, totals, picker. The data lives on conv.run
|
|
353
357
|
// (latest server snapshot folded into the conversation by
|
|
354
358
|
// foldRunIntoConversation); for runs that have not yet polled we
|
|
@@ -360,6 +364,31 @@ function renderActive() {
|
|
|
360
364
|
document.title = conv.title ? conv.title : 'AI Hub';
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
// Render the inline employee selector shown in the coach section of an
|
|
368
|
+
// active conversation. Allows switching agents without reopening the modal.
|
|
369
|
+
function renderActiveEmployeeSelect(conv) {
|
|
370
|
+
const sel = els['active-employee-select'];
|
|
371
|
+
if (!sel) return;
|
|
372
|
+
const employees = state.bootstrap?.employees || [];
|
|
373
|
+
if (employees.length === 0) { sel.hidden = true; return; }
|
|
374
|
+
sel.hidden = false;
|
|
375
|
+
// Only rebuild the options when the employee list changes (avoid reset on every poll).
|
|
376
|
+
const newKey = employees.map((e) => `${e.id}:${e.available}`).join('|');
|
|
377
|
+
if (sel.dataset.optionsKey !== newKey) {
|
|
378
|
+
sel.innerHTML = '';
|
|
379
|
+
for (const emp of employees) {
|
|
380
|
+
const opt = document.createElement('option');
|
|
381
|
+
opt.value = emp.id;
|
|
382
|
+
opt.textContent = emp.label + (emp.available ? '' : ' (unavailable)');
|
|
383
|
+
if (!emp.available) opt.disabled = true;
|
|
384
|
+
sel.appendChild(opt);
|
|
385
|
+
}
|
|
386
|
+
sel.dataset.optionsKey = newKey;
|
|
387
|
+
}
|
|
388
|
+
const current = (conv && conv.employeeId) || state.selectedEmployeeId || 'claude';
|
|
389
|
+
if (sel.value !== current) sel.value = current;
|
|
390
|
+
}
|
|
391
|
+
|
|
363
392
|
// Issue #347 R1 — render the pizza tracker. Reads conv.run.stages and
|
|
364
393
|
// conv.run.currentPhase from the most recent backend snapshot. Hidden
|
|
365
394
|
// when the active job has no declared phases.
|
|
@@ -1088,6 +1117,7 @@ const FRAIM_INVOCATION_SYMBOL = {
|
|
|
1088
1117
|
};
|
|
1089
1118
|
|
|
1090
1119
|
function fraimInvocationFor(employeeId, jobId, kind) {
|
|
1120
|
+
if (jobId === '__freeform__') return null;
|
|
1091
1121
|
const symbol = FRAIM_INVOCATION_SYMBOL[employeeId] || '/fraim';
|
|
1092
1122
|
if (kind === 'start') {
|
|
1093
1123
|
return `${symbol} ${jobId}`;
|
|
@@ -1098,11 +1128,14 @@ function fraimInvocationFor(employeeId, jobId, kind) {
|
|
|
1098
1128
|
// Wrap the manager's typed instructions with the host-appropriate FRAIM
|
|
1099
1129
|
// invocation. The wrapped text is what we ACTUALLY send to the host CLI
|
|
1100
1130
|
// AND what we show in the timeline so the manager sees what the agent
|
|
1101
|
-
// received.
|
|
1131
|
+
// received. For freeform jobs (no FRAIM job assigned), the instructions
|
|
1132
|
+
// are sent verbatim with no invocation prefix.
|
|
1102
1133
|
function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
1134
|
+
const trimmed = (instructions || '').trim();
|
|
1103
1135
|
const invocation = fraimInvocationFor(employeeId, jobId, kind);
|
|
1136
|
+
// Freeform: no FRAIM prefix, no stub reference — just the raw instructions.
|
|
1137
|
+
if (!invocation) return trimmed;
|
|
1104
1138
|
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
1105
|
-
const trimmed = (instructions || '').trim();
|
|
1106
1139
|
if (!trimmed) return `${invocation}${stub}`;
|
|
1107
1140
|
// For continue turns, applyTemplateInvocation already writes the full
|
|
1108
1141
|
// FRAIM invocation (e.g. "/fraim follow-your-mentor") into the textarea.
|
|
@@ -1118,14 +1151,16 @@ async function startRun(job, instructions, employeeId) {
|
|
|
1118
1151
|
// Prefix the manager's typed instructions with the FRAIM invocation so
|
|
1119
1152
|
// the underlying host actually launches the right job. The prefixed
|
|
1120
1153
|
// text is what the host receives AND what we show in the timeline.
|
|
1121
|
-
|
|
1154
|
+
// Freeform jobs carry no job stub and no FRAIM invocation prefix.
|
|
1155
|
+
const isFreeform = job.id === '__freeform__';
|
|
1156
|
+
const absoluteStubPath = (!isFreeform && job.stubPath && state.projectPath)
|
|
1122
1157
|
? [state.projectPath, job.stubPath].join('/').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1123
|
-
:
|
|
1158
|
+
: undefined;
|
|
1124
1159
|
const agentMessage = buildAgentMessage(employeeId, job.id, 'start', instructions, absoluteStubPath);
|
|
1125
1160
|
const conv = {
|
|
1126
1161
|
id: newConversationId(),
|
|
1127
1162
|
projectPath: state.projectPath,
|
|
1128
|
-
title: deriveTitle(job.title, instructions),
|
|
1163
|
+
title: isFreeform ? deriveTitle('', instructions) : deriveTitle(job.title, instructions),
|
|
1129
1164
|
jobId: job.id,
|
|
1130
1165
|
jobTitle: job.title,
|
|
1131
1166
|
employeeId,
|
|
@@ -1170,6 +1205,43 @@ async function startRun(job, instructions, employeeId) {
|
|
|
1170
1205
|
}
|
|
1171
1206
|
}
|
|
1172
1207
|
|
|
1208
|
+
// Start a NEW run on an existing conversation with a different agent.
|
|
1209
|
+
// The conversation's message history stays intact; only the run restarts.
|
|
1210
|
+
async function restartConvWithAgent(conv, newAgentId, text) {
|
|
1211
|
+
const agentMessage = buildAgentMessage(newAgentId, conv.jobId, 'start', text);
|
|
1212
|
+
conv.status = 'running';
|
|
1213
|
+
conv.sessionId = null;
|
|
1214
|
+
conv.messages.push({ role: 'manager', text: agentMessage, at: Date.now() });
|
|
1215
|
+
upsertConversation(conv);
|
|
1216
|
+
renderRail();
|
|
1217
|
+
renderActive();
|
|
1218
|
+
try {
|
|
1219
|
+
const run = await requestJson('/api/ai-hub/runs', {
|
|
1220
|
+
method: 'POST',
|
|
1221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1222
|
+
body: JSON.stringify({
|
|
1223
|
+
projectPath: state.projectPath,
|
|
1224
|
+
hostId: newAgentId,
|
|
1225
|
+
jobId: conv.jobId,
|
|
1226
|
+
message: agentMessage,
|
|
1227
|
+
}),
|
|
1228
|
+
});
|
|
1229
|
+
conv.runId = run.id;
|
|
1230
|
+
foldRunIntoConversation(conv, run);
|
|
1231
|
+
upsertConversation(conv);
|
|
1232
|
+
renderRail();
|
|
1233
|
+
renderActive();
|
|
1234
|
+
startPolling();
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
conv.status = 'failed';
|
|
1237
|
+
conv.events.push({ channel: 'system', text: error.message });
|
|
1238
|
+
upsertConversation(conv);
|
|
1239
|
+
renderRail();
|
|
1240
|
+
renderActive();
|
|
1241
|
+
showStatus(error.message, true);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1173
1245
|
async function continueRun(text) {
|
|
1174
1246
|
const conv = activeConversation();
|
|
1175
1247
|
if (!conv || !conv.runId) return;
|
|
@@ -1320,6 +1392,31 @@ async function pickProject() {
|
|
|
1320
1392
|
}
|
|
1321
1393
|
}
|
|
1322
1394
|
|
|
1395
|
+
// Show step 2 of the new-job modal. Handles both regular jobs and freeform.
|
|
1396
|
+
function showStep2(job) {
|
|
1397
|
+
const isFreeform = job.id === '__freeform__';
|
|
1398
|
+
const assignedJobDiv = document.querySelector('#step2 .assigned-job');
|
|
1399
|
+
if (assignedJobDiv) assignedJobDiv.hidden = isFreeform;
|
|
1400
|
+
const h = document.querySelector('#step2 .modal-header h2');
|
|
1401
|
+
const p = document.querySelector('#step2 .modal-header p');
|
|
1402
|
+
if (isFreeform) {
|
|
1403
|
+
if (h) h.textContent = 'What do you need done?';
|
|
1404
|
+
if (p) p.textContent = 'Describe the task in plain language. No FRAIM job required.';
|
|
1405
|
+
els['instructions'].placeholder = 'Describe what you need…';
|
|
1406
|
+
} else {
|
|
1407
|
+
if (h) h.textContent = 'Tell the employee what you need';
|
|
1408
|
+
if (p) p.textContent = 'A few sentences is enough. The employee will ask if anything is unclear.';
|
|
1409
|
+
els['instructions'].placeholder = 'What outcome do you want? Any context the employee should know?';
|
|
1410
|
+
els['picked-name'].textContent = job.title;
|
|
1411
|
+
els['picked-desc'].textContent = job.intent || '';
|
|
1412
|
+
}
|
|
1413
|
+
els['instructions'].value = '';
|
|
1414
|
+
els['start'].disabled = true;
|
|
1415
|
+
els['step1'].hidden = true;
|
|
1416
|
+
els['step2'].hidden = false;
|
|
1417
|
+
setTimeout(() => els['instructions'].focus(), 50);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1323
1420
|
// ---------------------------------------------------------------------------
|
|
1324
1421
|
// Wire up
|
|
1325
1422
|
// ---------------------------------------------------------------------------
|
|
@@ -1334,14 +1431,15 @@ function wireEvents() {
|
|
|
1334
1431
|
});
|
|
1335
1432
|
els['next1'].addEventListener('click', () => {
|
|
1336
1433
|
if (!state.selectedJob) return;
|
|
1337
|
-
|
|
1338
|
-
els['picked-desc'].textContent = state.selectedJob.intent || '';
|
|
1339
|
-
els['instructions'].value = '';
|
|
1340
|
-
els['start'].disabled = true;
|
|
1341
|
-
els['step1'].hidden = true;
|
|
1342
|
-
els['step2'].hidden = false;
|
|
1343
|
-
setTimeout(() => els['instructions'].focus(), 50);
|
|
1434
|
+
showStep2(state.selectedJob);
|
|
1344
1435
|
});
|
|
1436
|
+
|
|
1437
|
+
if (els['freeform-btn']) {
|
|
1438
|
+
els['freeform-btn'].addEventListener('click', () => {
|
|
1439
|
+
state.selectedJob = { id: '__freeform__', title: 'Freeform task', intent: '' };
|
|
1440
|
+
showStep2(state.selectedJob);
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1345
1443
|
els['instructions'].addEventListener('input', () => {
|
|
1346
1444
|
els['start'].disabled = els['instructions'].value.trim().length === 0;
|
|
1347
1445
|
});
|
|
@@ -1365,9 +1463,32 @@ function wireEvents() {
|
|
|
1365
1463
|
if (!text) return;
|
|
1366
1464
|
els['coach-text'].value = '';
|
|
1367
1465
|
syncSendButton();
|
|
1368
|
-
|
|
1466
|
+
// If the user changed the agent via the inline selector, restart with the
|
|
1467
|
+
// new agent (new run) instead of continuing on the old one. This lets them
|
|
1468
|
+
// recover from a failed run by switching agents without opening the modal.
|
|
1469
|
+
const conv = activeConversation();
|
|
1470
|
+
const chosenAgent = els['active-employee-select'] && els['active-employee-select'].value;
|
|
1471
|
+
if (conv && chosenAgent && chosenAgent !== conv.employeeId && conv.status !== 'running') {
|
|
1472
|
+
conv.employeeId = chosenAgent;
|
|
1473
|
+
state.selectedEmployeeId = chosenAgent;
|
|
1474
|
+
await restartConvWithAgent(conv, chosenAgent, text);
|
|
1475
|
+
} else {
|
|
1476
|
+
await continueRun(text);
|
|
1477
|
+
}
|
|
1369
1478
|
});
|
|
1370
1479
|
|
|
1480
|
+
if (els['active-employee-select']) {
|
|
1481
|
+
els['active-employee-select'].addEventListener('change', () => {
|
|
1482
|
+
const conv = activeConversation();
|
|
1483
|
+
const newAgent = els['active-employee-select'].value;
|
|
1484
|
+
state.selectedEmployeeId = newAgent;
|
|
1485
|
+
if (conv) {
|
|
1486
|
+
conv.employeeId = newAgent;
|
|
1487
|
+
persistConversations();
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1371
1492
|
// Issue #347 R2 — template picker.
|
|
1372
1493
|
if (els['template-picker-btn']) {
|
|
1373
1494
|
els['template-picker-btn'].addEventListener('click', (e) => {
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -315,6 +315,28 @@ button { font: inherit; cursor: pointer; }
|
|
|
315
315
|
margin-bottom: 8px;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
.coach-title-row {
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: space-between;
|
|
322
|
+
margin-bottom: 8px;
|
|
323
|
+
}
|
|
324
|
+
.coach-title-row .section-title { margin-bottom: 0; }
|
|
325
|
+
.active-employee-row {
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
gap: 6px;
|
|
329
|
+
}
|
|
330
|
+
.active-employee-label {
|
|
331
|
+
font-size: 12px;
|
|
332
|
+
color: var(--muted);
|
|
333
|
+
}
|
|
334
|
+
.employee-select.inline {
|
|
335
|
+
font-size: 12px;
|
|
336
|
+
padding: 3px 8px;
|
|
337
|
+
border-radius: 6px;
|
|
338
|
+
}
|
|
339
|
+
|
|
318
340
|
/* Progress is now a single-line status strip (was a tall card) so the
|
|
319
341
|
messages region gets the vertical space it needs. */
|
|
320
342
|
.progress {
|