fraim-hub 2.0.274 → 2.0.275
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/catalog.js +26 -5
- package/dist/src/ai-hub/desktop-main.js +1 -1
- package/dist/src/ai-hub/office-sideload.js +12 -3
- package/dist/src/ai-hub/server.js +111 -29
- package/dist/src/config/persona-capability-bundles.js +3 -1
- package/extensions/office-word/taskpane.html +20 -11
- package/package.json +2 -2
- package/public/ai-hub/excel-taskpane/icon-64.png +0 -0
- package/public/ai-hub/excel-taskpane/index.html +298 -0
- package/public/ai-hub/excel-taskpane/manifest.xml +33 -0
- package/public/ai-hub/script.js +33 -11
|
@@ -379,6 +379,27 @@ function readJobFrontmatter(filePath) {
|
|
|
379
379
|
return null;
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Issue #1246: resolve the stub whose phase graph actually governs this job.
|
|
384
|
+
* A personalized override that is `extends`-only (no `phases`/`initialPhase`
|
|
385
|
+
* of its own) wins the layer-precedence override chain in findJobStubPath,
|
|
386
|
+
* but declares no phase graph AND has no `## Steps` section for
|
|
387
|
+
* loadJobPhasesFromSteps to fall back to either — every phase-derivation
|
|
388
|
+
* caller silently degraded to zero phases. Follow `extends` to the baseline
|
|
389
|
+
* stub when the resolved stub itself declares no phase graph.
|
|
390
|
+
*/
|
|
391
|
+
function resolveDeclaredPhaseStub(stubPath, projectPath) {
|
|
392
|
+
const fm = readJobFrontmatter(stubPath);
|
|
393
|
+
if (fm?.initialPhase && fm.phases)
|
|
394
|
+
return { fm, path: stubPath };
|
|
395
|
+
const extendsValue = typeof fm?.extends === 'string' ? fm.extends.trim() : '';
|
|
396
|
+
if (!extendsValue)
|
|
397
|
+
return { fm, path: stubPath };
|
|
398
|
+
const basePath = resolveExtendedStubPath(projectPath, extendsValue);
|
|
399
|
+
if (!basePath)
|
|
400
|
+
return { fm, path: stubPath };
|
|
401
|
+
return { fm: readJobFrontmatter(basePath), path: basePath };
|
|
402
|
+
}
|
|
382
403
|
function findJobStubPath(projectPath, jobId) {
|
|
383
404
|
// Walk both employee and manager layer sets. The analytics surfaces can
|
|
384
405
|
// inspect any FRAIM job run, including manager-side jobs such as
|
|
@@ -432,9 +453,9 @@ function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
|
432
453
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
433
454
|
if (!stubPath)
|
|
434
455
|
return [];
|
|
435
|
-
const fm =
|
|
456
|
+
const { fm, path: declaredPath } = resolveDeclaredPhaseStub(stubPath, projectPath);
|
|
436
457
|
if (!fm || !fm.initialPhase || !fm.phases)
|
|
437
|
-
return loadJobPhasesFromSteps(
|
|
458
|
+
return loadJobPhasesFromSteps(declaredPath);
|
|
438
459
|
const visited = new Set();
|
|
439
460
|
const ordered = [];
|
|
440
461
|
let cursor = fm.initialPhase;
|
|
@@ -461,7 +482,7 @@ function resolveJobPhaseTransition(jobId, projectPath, phaseId, outcome, discrim
|
|
|
461
482
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
462
483
|
if (!stubPath)
|
|
463
484
|
return null;
|
|
464
|
-
const fm =
|
|
485
|
+
const { fm } = resolveDeclaredPhaseStub(stubPath, projectPath);
|
|
465
486
|
if (!fm || !fm.phases)
|
|
466
487
|
return null;
|
|
467
488
|
const phaseDef = fm.phases[phaseId];
|
|
@@ -474,9 +495,9 @@ function loadAllJobPhaseIds(jobId, projectPath) {
|
|
|
474
495
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
475
496
|
if (!stubPath)
|
|
476
497
|
return new Set();
|
|
477
|
-
const fm =
|
|
498
|
+
const { fm, path: declaredPath } = resolveDeclaredPhaseStub(stubPath, projectPath);
|
|
478
499
|
if (!fm || !fm.phases) {
|
|
479
|
-
const phases = loadJobPhasesFromSteps(
|
|
500
|
+
const phases = loadJobPhasesFromSteps(declaredPath);
|
|
480
501
|
return new Set(phases.map((p) => p.id));
|
|
481
502
|
}
|
|
482
503
|
return new Set(Object.keys(fm.phases));
|
|
@@ -107,7 +107,7 @@ function configureAutoUpdater() {
|
|
|
107
107
|
function ensureWordSideload(projectPath, httpsPort) {
|
|
108
108
|
// Flag version bump: bump this string when new manifests are added so all
|
|
109
109
|
// users get re-sideloaded on their next launch.
|
|
110
|
-
const FLAG_VERSION = '
|
|
110
|
+
const FLAG_VERSION = 'v6-excel-addin';
|
|
111
111
|
const expectedFlag = `${FLAG_VERSION}:${httpsPort}`;
|
|
112
112
|
const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'word-sideloaded.flag');
|
|
113
113
|
const existingFlag = fs_1.default.existsSync(flagPath) ? fs_1.default.readFileSync(flagPath, 'utf8') : null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Sideloads Office add-in manifests (Word + PowerPoint) so they appear
|
|
4
|
-
* Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
|
|
3
|
+
* Sideloads Office add-in manifests (Word + PowerPoint + Excel) so they appear
|
|
4
|
+
* under Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
|
|
5
5
|
*
|
|
6
6
|
* Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\{<guid>}
|
|
7
7
|
* (Default) = ABSOLUTE FILE PATH to manifest.xml. This is the registry
|
|
@@ -52,6 +52,15 @@ const MANIFESTS = [
|
|
|
52
52
|
],
|
|
53
53
|
macContainer: 'com.microsoft.Powerpoint',
|
|
54
54
|
},
|
|
55
|
+
{
|
|
56
|
+
guid: 'da8d22c7-2c00-485a-bdc9-13dda7f80c48',
|
|
57
|
+
candidates: (base) => [
|
|
58
|
+
path_1.default.resolve(base, 'public/ai-hub/excel-taskpane/manifest.xml'),
|
|
59
|
+
path_1.default.resolve(__dirname, '..', '..', 'public/ai-hub/excel-taskpane/manifest.xml'),
|
|
60
|
+
path_1.default.resolve(__dirname, '..', '..', '..', 'public/ai-hub/excel-taskpane/manifest.xml'),
|
|
61
|
+
],
|
|
62
|
+
macContainer: 'com.microsoft.Excel',
|
|
63
|
+
},
|
|
55
64
|
];
|
|
56
65
|
function resolveManifestPath(entry, projectPath) {
|
|
57
66
|
return entry.candidates(projectPath).find(c => fs_1.default.existsSync(c)) ?? null;
|
|
@@ -157,7 +166,7 @@ function sideloadManifest(projectPath, options = {}) {
|
|
|
157
166
|
for (const entry of MANIFESTS) {
|
|
158
167
|
const manifestPath = resolveManifestPath(entry, projectPath);
|
|
159
168
|
if (!manifestPath) {
|
|
160
|
-
return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/ and public/ai-hub/
|
|
169
|
+
return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/, public/ai-hub/powerpoint-taskpane/, and public/ai-hub/excel-taskpane/` };
|
|
161
170
|
}
|
|
162
171
|
const sideloadPath = prepareManifestForSideload(entry, manifestPath, options);
|
|
163
172
|
if (process.platform === 'win32') {
|
|
@@ -248,15 +248,18 @@ function getCustomPersonaForJob(projectPath, jobId) {
|
|
|
248
248
|
const match = employees.find((e) => Array.isArray(e.jobIds) && e.jobIds.includes(jobId));
|
|
249
249
|
return match ? match.key : null;
|
|
250
250
|
}
|
|
251
|
+
// This set exists only for jobs that need hiding from the main picker for a
|
|
252
|
+
// reason beyond ownership. `praise-fraim` is already surfaced in the Hub
|
|
253
|
+
// coaching UI, so a picker entry would be a duplicate. Every other FRAIM-meta
|
|
254
|
+
// job (contribute-to-fraim, file-fraim-issue) or persona-owned job
|
|
255
|
+
// (run-on-remote-hub, setup-remote-hub under sreya) is a real, synced
|
|
256
|
+
// capability and belongs in the main picker like any other job.
|
|
257
|
+
// `evolve-fraim-registry` is a personalized-employee-only job local to the
|
|
258
|
+
// FRAIM source repo (no registry/ counterpart ships to any installation), so
|
|
259
|
+
// it has no entry here at all — this set is shared product code and must not
|
|
260
|
+
// carry a job id that is meaningless to every customer.
|
|
251
261
|
const FRAIM_INTERNAL_JOB_IDS = new Set([
|
|
252
|
-
'contribute-to-fraim',
|
|
253
|
-
'create-registry-asset',
|
|
254
|
-
'extract-ashley-learnings',
|
|
255
|
-
'file-fraim-issue',
|
|
256
262
|
'praise-fraim',
|
|
257
|
-
'run-on-remote-hub',
|
|
258
|
-
'setup-remote-hub',
|
|
259
|
-
'update-registry-override',
|
|
260
263
|
]);
|
|
261
264
|
// Jobs that operate on machine-level context (~/.fraim/) rather than a project
|
|
262
265
|
// directory. They must be launchable from the Hub even when no project has been
|
|
@@ -1004,6 +1007,23 @@ function readPersistedRunProjection(conversation) {
|
|
|
1004
1007
|
runDiscriminant: typeof value.runDiscriminant === 'string' ? value.runDiscriminant : null,
|
|
1005
1008
|
};
|
|
1006
1009
|
}
|
|
1010
|
+
// Issue #1246: the disk-serving GET-by-id endpoint returned the persisted
|
|
1011
|
+
// `run.stages` snapshot verbatim. That snapshot is written mid-run (e.g. while
|
|
1012
|
+
// a personalized `extends`-only stub was still resolving to zero phases) and
|
|
1013
|
+
// never revisited, so a completed run's tracker could freeze on one stale
|
|
1014
|
+
// phase forever. Re-derive stages from the conversation's own phase history
|
|
1015
|
+
// at read time instead of trusting the stored value.
|
|
1016
|
+
function deriveStagesForConversation(conversation) {
|
|
1017
|
+
const persisted = readPersistedRunProjection(conversation);
|
|
1018
|
+
return deriveStages({
|
|
1019
|
+
jobId: conversation.jobId,
|
|
1020
|
+
status: conversation.status,
|
|
1021
|
+
currentPhase: persisted?.currentPhase ?? null,
|
|
1022
|
+
phaseHistory: persisted?.phaseHistory ?? [],
|
|
1023
|
+
phaseVisits: persisted?.phaseVisits ?? [],
|
|
1024
|
+
runDiscriminant: persisted?.runDiscriminant ?? undefined,
|
|
1025
|
+
}, conversation.projectPath);
|
|
1026
|
+
}
|
|
1007
1027
|
function boundedText(value, maxChars = 500) {
|
|
1008
1028
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
1009
1029
|
if (text.length <= maxChars)
|
|
@@ -1226,6 +1246,7 @@ function appendHostMessage(run, hostId, event, channel) {
|
|
|
1226
1246
|
}
|
|
1227
1247
|
const MAX_PROJECTED_EVENT_CHARS = 300;
|
|
1228
1248
|
const MAX_PROJECTED_EVENTS_PER_RUN = 200;
|
|
1249
|
+
const MAX_HOST_ERROR_THREAD_CHARS = 160;
|
|
1229
1250
|
function boundedEventText(text) {
|
|
1230
1251
|
const trimmed = text.trim();
|
|
1231
1252
|
if (trimmed.length <= MAX_PROJECTED_EVENT_CHARS)
|
|
@@ -1378,12 +1399,6 @@ function phaseVisitsForProjection(run) {
|
|
|
1378
1399
|
latestText: entry.latestText,
|
|
1379
1400
|
}));
|
|
1380
1401
|
}
|
|
1381
|
-
// Build the stage list for a run. Combines the FSM's reachable path with
|
|
1382
|
-
// any phases the run has actually visited (in case the run took an
|
|
1383
|
-
// onFailure back-edge into a phase the simple onSuccess walk wouldn't
|
|
1384
|
-
// surface). Each stage is marked done / current / upcoming based on
|
|
1385
|
-
// whether it precedes / matches / follows the current phase along the
|
|
1386
|
-
// rendered order.
|
|
1387
1402
|
function deriveStages(run, projectPath) {
|
|
1388
1403
|
const declaredPath = (0, catalog_1.loadJobPhases)(run.jobId, projectPath, run.runDiscriminant || 'feature');
|
|
1389
1404
|
if (declaredPath.length === 0)
|
|
@@ -1832,18 +1847,29 @@ function buildHubRecoveryContinueMessage(run, exitCode, attempt) {
|
|
|
1832
1847
|
function hostErrorSuffix(run) {
|
|
1833
1848
|
return run.lastHostError ? ` Last host error: ${run.lastHostError}` : '';
|
|
1834
1849
|
}
|
|
1835
|
-
// Issue #
|
|
1836
|
-
//
|
|
1837
|
-
//
|
|
1838
|
-
//
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1850
|
+
// Issue #1240: raw API error messages (rate-limit notices, turn.failed payloads)
|
|
1851
|
+
// can be multi-sentence paragraphs with retry hints, URLs, and quota details.
|
|
1852
|
+
// The conversation thread should show only the first sentence, capped at 160
|
|
1853
|
+
// chars; full technical detail remains in run.events (micro-log).
|
|
1854
|
+
function summarizeHostErrorForThread(error) {
|
|
1855
|
+
const raw = String(error || '').trim();
|
|
1856
|
+
if (!raw)
|
|
1857
|
+
return 'An unexpected error occurred.';
|
|
1858
|
+
const firstLine = raw.split(/[\r\n]/)[0].trim();
|
|
1859
|
+
// Split on ". " followed by an uppercase letter to find the sentence boundary.
|
|
1860
|
+
const sentenceEnd = firstLine.search(/\.\s+[A-Z]/);
|
|
1861
|
+
const firstSentence = sentenceEnd >= 0 ? firstLine.slice(0, sentenceEnd + 1) : firstLine;
|
|
1862
|
+
if (firstSentence.length <= MAX_HOST_ERROR_THREAD_CHARS)
|
|
1863
|
+
return firstSentence;
|
|
1864
|
+
return firstSentence.slice(0, MAX_HOST_ERROR_THREAD_CHARS - 3) + '…';
|
|
1865
|
+
}
|
|
1866
|
+
// Only surface a host error in the conversation thread when the run has
|
|
1867
|
+
// actually terminated with a failure. Transient errors that resolve through
|
|
1868
|
+
// auto-recovery must not reach the thread so the manager is not alarmed by
|
|
1869
|
+
// noise that the system handled on its own.
|
|
1844
1870
|
function pushTerminalFailureToThread(run, exitCode) {
|
|
1845
1871
|
if (exitCode !== 0 && run.lastHostError) {
|
|
1846
|
-
run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${run.lastHostError}`));
|
|
1872
|
+
run.messages.push((0, hosts_1.createHubMessage)('system', `This run stopped: ${summarizeHostErrorForThread(run.lastHostError)}`));
|
|
1847
1873
|
}
|
|
1848
1874
|
}
|
|
1849
1875
|
function createHubRecoveryEvent(run, exitCode, attempt) {
|
|
@@ -2236,7 +2262,7 @@ class AiHubServer {
|
|
|
2236
2262
|
});
|
|
2237
2263
|
});
|
|
2238
2264
|
}
|
|
2239
|
-
this.app.get(['/word-taskpane/config.js', '/powerpoint-taskpane/config.js'], (_req, res) => {
|
|
2265
|
+
this.app.get(['/word-taskpane/config.js', '/powerpoint-taskpane/config.js', '/excel-taskpane/config.js'], (_req, res) => {
|
|
2240
2266
|
const port = this.httpPort || 43091;
|
|
2241
2267
|
const origin = `http://127.0.0.1:${port}`;
|
|
2242
2268
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
@@ -2529,6 +2555,16 @@ class AiHubServer {
|
|
|
2529
2555
|
...job,
|
|
2530
2556
|
requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
|
|
2531
2557
|
}));
|
|
2558
|
+
// Issue #1247: expose internal jobs in a separate bootstrap field so next-job
|
|
2559
|
+
// recommendation chips can resolve and launch them. They are intentionally
|
|
2560
|
+
// excluded from `jobs` (the main picker list) but must be resolvable for chips.
|
|
2561
|
+
// Internal jobs default to the fraimworker persona rather than the unassigned default.
|
|
2562
|
+
const internalJobs = rawJobs
|
|
2563
|
+
.filter((job) => FRAIM_INTERNAL_JOB_IDS.has(job.id))
|
|
2564
|
+
.map((job) => ({
|
|
2565
|
+
...job,
|
|
2566
|
+
requiredPersonaKey: getProtectedPersonaForHubJob(job.id) ?? GENERIC_WORKER_PERSONA_KEY,
|
|
2567
|
+
}));
|
|
2532
2568
|
const managerTemplates = (0, catalog_1.discoverManagerTemplates)(normalizedProjectPath, catalogOptions);
|
|
2533
2569
|
// Issue #750: the apiKey always comes from ~/.fraim/config.json — no header
|
|
2534
2570
|
// override, no ai-hub-state.json copy, no fallback chain.
|
|
@@ -2579,6 +2615,7 @@ class AiHubServer {
|
|
|
2579
2615
|
preferences: { ...preferences, apiKey: resolvedApiKey },
|
|
2580
2616
|
categories: (0, catalog_1.getAiHubCategories)(normalizedProjectPath, catalogOptions),
|
|
2581
2617
|
jobs,
|
|
2618
|
+
internalJobs,
|
|
2582
2619
|
managerTemplates,
|
|
2583
2620
|
employees,
|
|
2584
2621
|
configuredAgents,
|
|
@@ -2755,14 +2792,15 @@ class AiHubServer {
|
|
|
2755
2792
|
}
|
|
2756
2793
|
}
|
|
2757
2794
|
// Issue #1221: a host-reported error/turn-failure must never be dropped
|
|
2758
|
-
// as opaque raw JSON. Record it for the terminal exit message
|
|
2759
|
-
// in
|
|
2760
|
-
// panel), and put it in Micro-manage unconditionally — bypassing
|
|
2795
|
+
// as opaque raw JSON. Record it for the terminal exit message and put it
|
|
2796
|
+
// in Micro-manage unconditionally — bypassing
|
|
2761
2797
|
// projectHostEventForConversation's raw-payload filter, which exists for
|
|
2762
2798
|
// routine protocol chatter, not for a failure the manager needs to see.
|
|
2799
|
+
// Issue #1240: do NOT push to run.messages here; only surface in the
|
|
2800
|
+
// conversation thread via pushTerminalFailureToThread when the run actually
|
|
2801
|
+
// fails, so transient errors resolved by auto-recovery stay silent to the user.
|
|
2763
2802
|
if (event.hostError) {
|
|
2764
2803
|
run.lastHostError = event.hostError;
|
|
2765
|
-
run.messages.push((0, hosts_1.createHubMessage)('system', `Host error: ${event.hostError}`));
|
|
2766
2804
|
run.events.push((0, hosts_1.createHubEvent)('stderr', boundedEventText(event.hostError)));
|
|
2767
2805
|
}
|
|
2768
2806
|
const projected = projectHostEventForConversation(event, channel);
|
|
@@ -4198,6 +4236,45 @@ class AiHubServer {
|
|
|
4198
4236
|
res.end(data);
|
|
4199
4237
|
});
|
|
4200
4238
|
});
|
|
4239
|
+
// Issue #1248: Serve the Excel task pane HTML and manifest, mirroring the
|
|
4240
|
+
// PowerPoint route above. Office JS appends query strings
|
|
4241
|
+
// (?_host_Info=Excel$Win32$...) to every request, so strip them before
|
|
4242
|
+
// resolving the file path.
|
|
4243
|
+
this.app.get(/^\/excel-taskpane(\/.*)?$/, (req, res) => {
|
|
4244
|
+
const taskpaneDir = resolveTaskpaneDir('excel-taskpane');
|
|
4245
|
+
const { pathname } = new URL(req.url, 'http://localhost');
|
|
4246
|
+
const relativePath = pathname.replace(/^\/excel-taskpane\/?/, '') || 'index.html';
|
|
4247
|
+
const filePath = path_1.default.join(taskpaneDir, relativePath);
|
|
4248
|
+
// Path traversal guard. A bare `startsWith(taskpaneDir)` (the pattern
|
|
4249
|
+
// the PowerPoint route above uses) would also accept a sibling
|
|
4250
|
+
// directory whose name happens to start with "excel-taskpane" (e.g.
|
|
4251
|
+
// "excel-taskpane-evil") — require the separator (or an exact match)
|
|
4252
|
+
// like the /_fraim-hub-ui asset guard above does.
|
|
4253
|
+
if (!filePath.startsWith(taskpaneDir + path_1.default.sep) && filePath !== taskpaneDir) {
|
|
4254
|
+
res.status(403).end();
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
4258
|
+
const contentTypeMap = {
|
|
4259
|
+
'.html': 'text/html; charset=utf-8',
|
|
4260
|
+
'.xml': 'application/xml',
|
|
4261
|
+
'.js': 'application/javascript',
|
|
4262
|
+
'.css': 'text/css',
|
|
4263
|
+
'.png': 'image/png',
|
|
4264
|
+
'.ico': 'image/x-icon',
|
|
4265
|
+
'.svg': 'image/svg+xml',
|
|
4266
|
+
};
|
|
4267
|
+
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
|
4268
|
+
fs_1.default.readFile(filePath, (err, data) => {
|
|
4269
|
+
if (err) {
|
|
4270
|
+
res.status(404).end('Not found');
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
res.setHeader('Content-Type', contentType);
|
|
4274
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
4275
|
+
res.end(data);
|
|
4276
|
+
});
|
|
4277
|
+
});
|
|
4201
4278
|
this.app.get('/api/ai-hub/bootstrap', async (req, res) => {
|
|
4202
4279
|
let projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
|
|
4203
4280
|
? req.query.projectPath
|
|
@@ -4365,8 +4442,13 @@ class AiHubServer {
|
|
|
4365
4442
|
const conversation = this.conversationStore.loadConversation(key, conversationId);
|
|
4366
4443
|
if (!conversation)
|
|
4367
4444
|
return res.status(404).json({ error: 'conversation not found' });
|
|
4445
|
+
// Issue #1246: re-derive stages at read time rather than trusting the
|
|
4446
|
+
// persisted mid-run snapshot; see deriveStagesForConversation.
|
|
4447
|
+
const withFreshStages = conversation.run
|
|
4448
|
+
? { ...conversation, run: { ...conversation.run, stages: deriveStagesForConversation(conversation) } }
|
|
4449
|
+
: conversation;
|
|
4368
4450
|
// Issue #1090: ownership is derived on read, never served from the frozen record.
|
|
4369
|
-
return res.json({ projectPath: key, scope: scope ?? 'project', conversation: withDerivedPersona(
|
|
4451
|
+
return res.json({ projectPath: key, scope: scope ?? 'project', conversation: withDerivedPersona(withFreshStages, key), source: 'disk' });
|
|
4370
4452
|
}
|
|
4371
4453
|
if (req.query.headersOnly === '1' || req.query.headersOnly === 'true') {
|
|
4372
4454
|
// One disk walk for the whole list, not one per conversation.
|
|
@@ -219,7 +219,9 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
219
219
|
personaKey: 'sreya',
|
|
220
220
|
bundleId: 'persona-sreya-core',
|
|
221
221
|
catalogMetadata: buildCatalogMetadata('sreya', ['slo-design-and-implementation', 'incident-response', 'reliability-review']),
|
|
222
|
-
|
|
222
|
+
// run-on-remote-hub and setup-remote-hub are owned by SReya, the infra
|
|
223
|
+
// persona, rather than the display-only FRAIMworker default.
|
|
224
|
+
protectedJobs: ['slo-design-and-implementation', 'incident-response', 'reliability-review', 'production-readiness-review', 'run-on-remote-hub', 'setup-remote-hub'],
|
|
223
225
|
protectedAliases: ['sre', 'reliability', 'incident-response', 'on-call'],
|
|
224
226
|
defaultHireMode: 'job',
|
|
225
227
|
lockCopy: 'Hire SREya to unlock SLO governance, incident response, and production reliability work for this request.'
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>FRAIM Hub</title>
|
|
7
|
-
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
|
|
8
|
-
<script src="config.js" type="text/javascript"></script>
|
|
6
|
+
<title>FRAIM Hub</title>
|
|
7
|
+
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
|
|
8
|
+
<script src="config.js" type="text/javascript"></script>
|
|
9
9
|
<style>
|
|
10
10
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
11
|
html, body { height: 100%; overflow: hidden; }
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// Loading an HTTP iframe from an HTTPS page is mixed-content-blocked, so use the
|
|
20
20
|
// same origin as the taskpane when running over HTTPS (the ssl-proxy forwards all
|
|
21
21
|
// routes to the Hub). HTTP (isolated tests) keeps the direct Hub address.
|
|
22
|
-
var HUB_ORIGIN = window.location.protocol === 'https:'
|
|
23
|
-
? window.location.origin
|
|
24
|
-
: (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
|
|
22
|
+
var HUB_ORIGIN = window.location.protocol === 'https:'
|
|
23
|
+
? window.location.origin
|
|
24
|
+
: (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
|
|
25
25
|
var hubFrame = document.getElementById('hub');
|
|
26
26
|
var pendingPush = null; // context queued before hub-ready fires
|
|
27
27
|
var hubReady = false;
|
|
@@ -70,12 +70,21 @@
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function readBodyAsync(cb) {
|
|
73
|
+
// Issue #1239: getSelectedDataAsync (used by readSelectionAsync above) only
|
|
74
|
+
// ever returns the current selection, never the full document — passing a
|
|
75
|
+
// different `valueFormat`/omitting `filterType` does not change that. Use
|
|
76
|
+
// the Word-specific API (already used below for comments/insert/append) to
|
|
77
|
+
// read the actual document body.
|
|
73
78
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
if (typeof Word !== 'undefined' && Word.run) {
|
|
80
|
+
Word.run(function(ctx) {
|
|
81
|
+
var body = ctx.document.body;
|
|
82
|
+
body.load('text');
|
|
83
|
+
return ctx.sync().then(function() { cb(body.text || ''); });
|
|
84
|
+
}).catch(function() { cb(''); });
|
|
85
|
+
} else {
|
|
86
|
+
cb('');
|
|
87
|
+
}
|
|
79
88
|
} catch(e) { cb(''); }
|
|
80
89
|
}
|
|
81
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.275",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fraim-hub": "bin/fraim-hub.js",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"electron-updater": "^6.8.9",
|
|
169
169
|
"express": "^5.2.1",
|
|
170
170
|
"extract-zip": "^2.0.1",
|
|
171
|
-
"fraim": "2.0.
|
|
171
|
+
"fraim": "2.0.275",
|
|
172
172
|
"mongodb": "^7.0.0",
|
|
173
173
|
"node-cron": "4.2.1",
|
|
174
174
|
"node-edge-tts": "^1.2.10",
|
|
Binary file
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FRAIM Hub</title>
|
|
7
|
+
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript"></script>
|
|
8
|
+
<script src="config.js" type="text/javascript"></script>
|
|
9
|
+
<style>
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
html, body { height: 100%; overflow: hidden; }
|
|
12
|
+
iframe { width: 100%; height: 100vh; border: none; display: block; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<iframe id="hub" src="" allow="clipboard-read; clipboard-write"></iframe>
|
|
17
|
+
<script>
|
|
18
|
+
// Excel task pane — mirrors extensions/office-word/taskpane.html and
|
|
19
|
+
// public/ai-hub/powerpoint-taskpane/index.html so Excel gets the identical
|
|
20
|
+
// full Hub experience (issue #1248). Mounts the Hub /ai-hub/ surface in an
|
|
21
|
+
// iframe and bridges sheet/selection context over postMessage using the
|
|
22
|
+
// same word-context / word-request / word-response contract the Hub
|
|
23
|
+
// already speaks for every host (the names are the generic host-bridge
|
|
24
|
+
// protocol, not Word-specific).
|
|
25
|
+
//
|
|
26
|
+
// In Excel Online the pane is served over HTTPS (ssl-proxy at localhost:43092,
|
|
27
|
+
// same mechanism Word Online already uses). Loading an HTTP iframe from an
|
|
28
|
+
// HTTPS page is mixed-content-blocked, so use the pane's own origin there.
|
|
29
|
+
// On desktop (HTTP) use the direct Hub address.
|
|
30
|
+
var HUB_ORIGIN = window.location.protocol === 'https:'
|
|
31
|
+
? window.location.origin
|
|
32
|
+
: (window.FRAIM_HUB_ORIGIN || 'http://127.0.0.1:43091');
|
|
33
|
+
var hubFrame = document.getElementById('hub');
|
|
34
|
+
var pendingPush = null; // context queued before hub-ready fires
|
|
35
|
+
var hubReady = false;
|
|
36
|
+
var selectionHandlerAdded = false;
|
|
37
|
+
// Selection captured the last time context was read (on mount, on
|
|
38
|
+
// get-context/get-selection, and on every DocumentSelectionChanged fire).
|
|
39
|
+
// Write-back (R4) needs this to decide adjacent-cell vs append-new-row,
|
|
40
|
+
// since Excel's hasSelection is always true and can't gate that branch
|
|
41
|
+
// the way Word's optional selection does.
|
|
42
|
+
var lastSelectionInfo = null;
|
|
43
|
+
|
|
44
|
+
// ── postMessage bridge (inbound from Hub) ─────────────────────────────────
|
|
45
|
+
window.addEventListener('message', function(event) {
|
|
46
|
+
if (event.origin !== HUB_ORIGIN) return;
|
|
47
|
+
var msg = event.data || {};
|
|
48
|
+
if (msg.type === 'hub-ready') {
|
|
49
|
+
hubReady = true;
|
|
50
|
+
if (pendingPush) { pushToHub(pendingPush); pendingPush = null; }
|
|
51
|
+
} else if (msg.type === 'word-request') {
|
|
52
|
+
handleExcelRequest(msg.action, msg.requestId, msg.payload || {});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function pushToHub(msg) {
|
|
57
|
+
if (hubFrame && hubFrame.contentWindow) {
|
|
58
|
+
hubFrame.contentWindow.postMessage(msg, HUB_ORIGIN);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Reading Excel context ───────────────────────────────────────────────────
|
|
63
|
+
function docMeta() {
|
|
64
|
+
var meta = { docUrl: '', docTitle: '' };
|
|
65
|
+
try {
|
|
66
|
+
var doc = window.Office && Office.context && Office.context.document;
|
|
67
|
+
meta.docUrl = (doc && doc.url) ? doc.url : '';
|
|
68
|
+
if (meta.docUrl) {
|
|
69
|
+
var parts = meta.docUrl.replace(/\\/g, '/').split('/');
|
|
70
|
+
meta.docTitle = (parts[parts.length - 1] || '').replace(/\.[^.]+$/, '');
|
|
71
|
+
}
|
|
72
|
+
} catch(e) {}
|
|
73
|
+
return meta;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// R2 edge case: Excel always has an active cell — there is no true "nothing
|
|
77
|
+
// selected" state the way Word has. Only a single cell at the sheet's bare
|
|
78
|
+
// A1 default counts as "nothing meaningful" for the R4 write-back branch.
|
|
79
|
+
function isDefaultA1(info) {
|
|
80
|
+
if (!info || !info.address) return true;
|
|
81
|
+
var bare = String(info.address).split('!').pop();
|
|
82
|
+
return bare === 'A1' || bare === '$A$1';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function csvEscapeCell(v) {
|
|
86
|
+
var s = (v === null || v === undefined) ? '' : String(v);
|
|
87
|
+
if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
|
|
88
|
+
return s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bounded CSV serialization of the used/visible range (R2, R8). Callers
|
|
92
|
+
// must still cap the RESULT to 800 chars — this only shapes the rows.
|
|
93
|
+
function rangeToCsv(values) {
|
|
94
|
+
return (values || []).map(function(row) {
|
|
95
|
+
return (row || []).map(csvEscapeCell).join(',');
|
|
96
|
+
}).join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Build the human-readable selection label, e.g. "D5 (Engineering OpEx,
|
|
100
|
+
// Q2 Forecast): 344400" — address, inferred row/column headers from the
|
|
101
|
+
// used range's first column/first row, and the cell's own value. Falls
|
|
102
|
+
// back to a bare "address: value" when no header is discoverable.
|
|
103
|
+
function describeSelection(selRange, usedRange) {
|
|
104
|
+
try {
|
|
105
|
+
var addr = String(selRange.address || '').split('!').pop();
|
|
106
|
+
var rows = (selRange.values && selRange.values[0]) || [];
|
|
107
|
+
var val = rows[0] !== undefined ? rows[0] : '';
|
|
108
|
+
var rowLabel = '', colLabel = '';
|
|
109
|
+
if (usedRange && !usedRange.isNullObject && usedRange.values && usedRange.values.length > 1) {
|
|
110
|
+
var relRow = selRange.rowIndex - usedRange.rowIndex;
|
|
111
|
+
var relCol = selRange.columnIndex - usedRange.columnIndex;
|
|
112
|
+
var headerRow = usedRange.values[0] || [];
|
|
113
|
+
var rowAtCol0 = (usedRange.values[relRow] || [])[0];
|
|
114
|
+
if (relRow > 0 && rowAtCol0 !== undefined && rowAtCol0 !== '') rowLabel = String(rowAtCol0);
|
|
115
|
+
if (relCol > 0 && headerRow[relCol] !== undefined && headerRow[relCol] !== '') colLabel = String(headerRow[relCol]);
|
|
116
|
+
}
|
|
117
|
+
var parts = [rowLabel, colLabel].filter(Boolean);
|
|
118
|
+
return parts.length ? (addr + ' (' + parts.join(', ') + '): ' + val) : (addr + ': ' + val);
|
|
119
|
+
} catch(e) { return ''; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function emptyContext() {
|
|
123
|
+
var meta = docMeta();
|
|
124
|
+
return { docUrl: meta.docUrl, docTitle: meta.docTitle, selection: '', hasSelection: true, bodyPreview: '', wordCount: 0, comments: [] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readFullContext(cb) {
|
|
128
|
+
var meta = docMeta();
|
|
129
|
+
if (typeof Excel === 'undefined' || !Excel.run) { cb(emptyContext()); return; }
|
|
130
|
+
Excel.run(function(ctx) {
|
|
131
|
+
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
|
|
132
|
+
sheet.load('name');
|
|
133
|
+
var selRange = ctx.workbook.getSelectedRange();
|
|
134
|
+
selRange.load('address,values,rowIndex,columnIndex');
|
|
135
|
+
var usedRange = sheet.getUsedRangeOrNullObject();
|
|
136
|
+
usedRange.load('values,isNullObject,rowIndex,columnIndex,rowCount');
|
|
137
|
+
var comments = sheet.comments;
|
|
138
|
+
comments.load('items/content,items/authorName,items/resolved');
|
|
139
|
+
return ctx.sync().then(function() {
|
|
140
|
+
lastSelectionInfo = { address: selRange.address, rowIndex: selRange.rowIndex, columnIndex: selRange.columnIndex };
|
|
141
|
+
var body = usedRange.isNullObject ? '' : rangeToCsv(usedRange.values);
|
|
142
|
+
var commentList = (comments.items || []).slice(0, 20).map(function(c) {
|
|
143
|
+
return { author: c.authorName || '', text: c.content || '', resolved: !!c.resolved };
|
|
144
|
+
});
|
|
145
|
+
cb({
|
|
146
|
+
docUrl: meta.docUrl,
|
|
147
|
+
docTitle: sheet.name ? (meta.docTitle + ' — ' + sheet.name) : meta.docTitle,
|
|
148
|
+
selection: describeSelection(selRange, usedRange),
|
|
149
|
+
hasSelection: true,
|
|
150
|
+
bodyPreview: body.slice(0, 800),
|
|
151
|
+
wordCount: body ? body.split(/\s+/).filter(Boolean).length : 0,
|
|
152
|
+
comments: commentList,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}).catch(function() { cb(emptyContext()); });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Lighter-weight read used by get-selection and the selection-change
|
|
159
|
+
// handler — just the label, not the whole used range + comments.
|
|
160
|
+
function refreshSelectionLabel(cb) {
|
|
161
|
+
if (typeof Excel === 'undefined' || !Excel.run) { cb(''); return; }
|
|
162
|
+
Excel.run(function(ctx) {
|
|
163
|
+
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
|
|
164
|
+
var selRange = ctx.workbook.getSelectedRange();
|
|
165
|
+
selRange.load('address,values,rowIndex,columnIndex');
|
|
166
|
+
var usedRange = sheet.getUsedRangeOrNullObject();
|
|
167
|
+
usedRange.load('values,isNullObject,rowIndex,columnIndex');
|
|
168
|
+
return ctx.sync().then(function() {
|
|
169
|
+
lastSelectionInfo = { address: selRange.address, rowIndex: selRange.rowIndex, columnIndex: selRange.columnIndex };
|
|
170
|
+
cb(describeSelection(selRange, usedRange));
|
|
171
|
+
});
|
|
172
|
+
}).catch(function() { cb(''); });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Selection-change handler ───────────────────────────────────────────────
|
|
176
|
+
// Reuses the same Office.context.document common-API event Word/PowerPoint
|
|
177
|
+
// already wire (issue #1248 spec: "the same Office JS event exists on
|
|
178
|
+
// Excel's document object") — it only signals that something changed; the
|
|
179
|
+
// actual range data is re-fetched via Excel.run() above, same as Word/PPT.
|
|
180
|
+
function addSelectionHandler() {
|
|
181
|
+
if (selectionHandlerAdded) return;
|
|
182
|
+
try {
|
|
183
|
+
Office.context.document.addHandlerAsync(
|
|
184
|
+
Office.EventType.DocumentSelectionChanged,
|
|
185
|
+
function() {
|
|
186
|
+
refreshSelectionLabel(function(sel) {
|
|
187
|
+
var m = docMeta();
|
|
188
|
+
pushToHub({ type: 'word-context-update', payload: {
|
|
189
|
+
docUrl: m.docUrl, docTitle: m.docTitle,
|
|
190
|
+
selection: sel, hasSelection: true,
|
|
191
|
+
}});
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
function(r) { if (r.status === Office.AsyncResultStatus.Succeeded) selectionHandlerAdded = true; }
|
|
195
|
+
);
|
|
196
|
+
} catch(e) {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Write-back (R4) ─────────────────────────────────────────────────────────
|
|
200
|
+
// Never overwrites the cell that was selected when the job started: if that
|
|
201
|
+
// selection was meaningful (anything but the sheet's bare A1 default),
|
|
202
|
+
// write into the cell immediately adjacent to it (same row, next column).
|
|
203
|
+
// Otherwise append a new row at the end of the used range. Either way, add
|
|
204
|
+
// a cell comment noting the write for audit/undo-by-review (R4's own
|
|
205
|
+
// recommended default) and rely on Excel's native Ctrl+Z for reject.
|
|
206
|
+
function writeBackNonDestructive(text, jobLabel, cb) {
|
|
207
|
+
if (typeof Excel === 'undefined' || !Excel.run) { cb(false, 'Excel API 1.10+ not available'); return; }
|
|
208
|
+
var info = lastSelectionInfo;
|
|
209
|
+
var meaningful = !!(info && !isDefaultA1(info));
|
|
210
|
+
var note = 'Written by FRAIM' + (jobLabel ? ' (' + jobLabel + ')' : '') + ' - ' + new Date().toISOString();
|
|
211
|
+
try {
|
|
212
|
+
Excel.run(function(ctx) {
|
|
213
|
+
var sheet = ctx.workbook.worksheets.getActiveWorksheet();
|
|
214
|
+
if (meaningful) {
|
|
215
|
+
var addr = String(info.address).split('!').pop();
|
|
216
|
+
var selRange = sheet.getRange(addr);
|
|
217
|
+
var target = selRange.getOffsetRange(0, 1);
|
|
218
|
+
target.values = [[text]];
|
|
219
|
+
sheet.comments.add(target, note);
|
|
220
|
+
return ctx.sync();
|
|
221
|
+
}
|
|
222
|
+
var used = sheet.getUsedRangeOrNullObject();
|
|
223
|
+
used.load('rowCount,rowIndex,isNullObject');
|
|
224
|
+
return ctx.sync().then(function() {
|
|
225
|
+
var nextRow = used.isNullObject ? 0 : (used.rowIndex + used.rowCount);
|
|
226
|
+
var target = sheet.getRangeByIndexes(nextRow, 0, 1, 1);
|
|
227
|
+
target.values = [[text]];
|
|
228
|
+
sheet.comments.add(target, note);
|
|
229
|
+
return ctx.sync();
|
|
230
|
+
});
|
|
231
|
+
}).then(function() { cb(true); }).catch(function(e) { cb(false, (e && e.message) || String(e)); });
|
|
232
|
+
} catch(e) { cb(false, e.message); }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Handling requests from Hub ─────────────────────────────────────────────
|
|
236
|
+
function handleExcelRequest(action, requestId, payload) {
|
|
237
|
+
function respond(result) {
|
|
238
|
+
pushToHub({ type: 'word-response', requestId: requestId, payload: result });
|
|
239
|
+
}
|
|
240
|
+
if (action === 'get-context') {
|
|
241
|
+
readFullContext(function(ctx) { respond(ctx); });
|
|
242
|
+
} else if (action === 'get-selection') {
|
|
243
|
+
refreshSelectionLabel(function(sel) { respond({ selection: sel, hasSelection: true }); });
|
|
244
|
+
} else if (action === 'insert-text') {
|
|
245
|
+
// Symmetry with Word/PowerPoint's action surface; not part of the
|
|
246
|
+
// write-back flow (which only ever calls insert-after/append-to-doc —
|
|
247
|
+
// see tryWordWriteBack in public/ai-hub/script.js). Sets the selected
|
|
248
|
+
// cell's own value directly, matching Word's "replace selection".
|
|
249
|
+
try {
|
|
250
|
+
if (typeof Excel === 'undefined' || !Excel.run) { respond({ ok: false, error: 'Excel API 1.10+ not available' }); return; }
|
|
251
|
+
Excel.run(function(ctx) {
|
|
252
|
+
var range = ctx.workbook.getSelectedRange();
|
|
253
|
+
range.values = [[payload.text || '']];
|
|
254
|
+
return ctx.sync();
|
|
255
|
+
}).then(function() { respond({ ok: true }); }).catch(function(e) { respond({ ok: false, error: e.message }); });
|
|
256
|
+
} catch(e) { respond({ ok: false, error: e.message }); }
|
|
257
|
+
} else if (action === 'insert-after' || action === 'append-to-doc') {
|
|
258
|
+
writeBackNonDestructive(payload.text || '', payload.jobLabel || '', function(ok, err) {
|
|
259
|
+
if (ok) respond({ ok: true }); else respond({ ok: false, error: err });
|
|
260
|
+
});
|
|
261
|
+
} else if (action === 'track-changes-on' || action === 'track-changes-off') {
|
|
262
|
+
// R4: Excel has no Office-JS-exposed equivalent to Word's tracked-changes
|
|
263
|
+
// mode. Respond with an explicit ok, not the generic unknown-action
|
|
264
|
+
// fallthrough PowerPoint's host script currently falls into for these
|
|
265
|
+
// same two action names.
|
|
266
|
+
respond({ ok: true });
|
|
267
|
+
} else {
|
|
268
|
+
respond({ ok: false, error: 'Unknown action: ' + action });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Mount ─────────────────────────────────────────────────────────────────
|
|
273
|
+
function mountHub() {
|
|
274
|
+
var meta = docMeta();
|
|
275
|
+
var params = new URLSearchParams({ surface: 'task-pane' });
|
|
276
|
+
if (meta.docUrl) params.set('docUrl', meta.docUrl);
|
|
277
|
+
if (meta.docTitle) params.set('docTitle', meta.docTitle);
|
|
278
|
+
hubFrame.src = HUB_ORIGIN + '/ai-hub/?' + params.toString();
|
|
279
|
+
|
|
280
|
+
hubFrame.addEventListener('load', function() {
|
|
281
|
+
setTimeout(function() {
|
|
282
|
+
readFullContext(function(ctx) {
|
|
283
|
+
var msg = { type: 'word-context', payload: ctx };
|
|
284
|
+
if (hubReady) { pushToHub(msg); } else { pendingPush = msg; }
|
|
285
|
+
});
|
|
286
|
+
}, 300);
|
|
287
|
+
addSelectionHandler();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (typeof Office !== 'undefined') {
|
|
292
|
+
Office.onReady(function() { mountHub(); });
|
|
293
|
+
} else {
|
|
294
|
+
window.addEventListener('DOMContentLoaded', mountHub);
|
|
295
|
+
}
|
|
296
|
+
</script>
|
|
297
|
+
</body>
|
|
298
|
+
</html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
|
3
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
4
|
+
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
|
5
|
+
xsi:type="TaskPaneApp">
|
|
6
|
+
<Id>da8d22c7-2c00-485a-bdc9-13dda7f80c48</Id>
|
|
7
|
+
<Version>1.0.0.0</Version>
|
|
8
|
+
<ProviderName>FRAIM</ProviderName>
|
|
9
|
+
<DefaultLocale>en-US</DefaultLocale>
|
|
10
|
+
<DisplayName DefaultValue="FRAIM"/>
|
|
11
|
+
<Description DefaultValue="AI Hub for your workbook - any FRAIM job, with sheet and selection context, in one task pane."/>
|
|
12
|
+
<IconUrl DefaultValue="https://localhost:43092/excel-taskpane/icon-64.png"/>
|
|
13
|
+
<HighResolutionIconUrl DefaultValue="https://localhost:43092/excel-taskpane/icon-64.png"/>
|
|
14
|
+
<AppDomains>
|
|
15
|
+
<AppDomain>https://appsforoffice.microsoft.com</AppDomain>
|
|
16
|
+
</AppDomains>
|
|
17
|
+
|
|
18
|
+
<Hosts>
|
|
19
|
+
<Host Name="Workbook"/>
|
|
20
|
+
</Hosts>
|
|
21
|
+
|
|
22
|
+
<Requirements>
|
|
23
|
+
<Sets>
|
|
24
|
+
<Set Name="ExcelApi" MinVersion="1.10"/>
|
|
25
|
+
</Sets>
|
|
26
|
+
</Requirements>
|
|
27
|
+
|
|
28
|
+
<DefaultSettings>
|
|
29
|
+
<SourceLocation DefaultValue="https://localhost:43092/excel-taskpane/"/>
|
|
30
|
+
</DefaultSettings>
|
|
31
|
+
|
|
32
|
+
<Permissions>ReadWriteDocument</Permissions>
|
|
33
|
+
</OfficeApp>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -5578,6 +5578,9 @@ function resolveNextJobFromCatalog(jobId) {
|
|
|
5578
5578
|
const all = [
|
|
5579
5579
|
...((state.bootstrap && state.bootstrap.jobs) || []),
|
|
5580
5580
|
...((state.bootstrap && state.bootstrap.managerTemplates) || []),
|
|
5581
|
+
// Issue #1247: internal jobs are excluded from the main picker list but must
|
|
5582
|
+
// be resolvable so next-job recommendation chips can launch them.
|
|
5583
|
+
...((state.bootstrap && state.bootstrap.internalJobs) || []),
|
|
5581
5584
|
];
|
|
5582
5585
|
return all.find((j) => j.id === jobId) || null;
|
|
5583
5586
|
}
|
|
@@ -7663,6 +7666,22 @@ function deriveTitle(jobTitle, instructions) {
|
|
|
7663
7666
|
// The browser sends raw manager instructions. AI Hub normalizes assigned
|
|
7664
7667
|
// FRAIM jobs into host-facing invocations so the Hub UI and channel callers
|
|
7665
7668
|
// share one start/continue contract.
|
|
7669
|
+
// Issue #1239: shared by startRun() and continueRun() — in a task-pane/
|
|
7670
|
+
// extension surface, fetches a fresh selection snapshot, merges it with the
|
|
7671
|
+
// last-pushed state.wordContext, and prepends the resulting document-context
|
|
7672
|
+
// block to outgoing text so the agent knows what the user is looking at. A
|
|
7673
|
+
// no-op outside task-pane/extension surfaces.
|
|
7674
|
+
async function withWordContext(text) {
|
|
7675
|
+
let effective = text || '';
|
|
7676
|
+
if (document.body.dataset.surface === 'task-pane' || document.body.dataset.surface === 'extension') {
|
|
7677
|
+
const fresh = await requestWordContext('get-selection').catch(() => null);
|
|
7678
|
+
const wc = (fresh && (fresh.selection || fresh.hasSelection)) ? { ...state.wordContext, ...fresh } : state.wordContext;
|
|
7679
|
+
const ctxBlock = buildWordContextBlock(wc);
|
|
7680
|
+
if (ctxBlock) effective = ctxBlock + '\n\n---\n\n' + effective;
|
|
7681
|
+
}
|
|
7682
|
+
return effective;
|
|
7683
|
+
}
|
|
7684
|
+
|
|
7666
7685
|
async function startRun(job, instructions, employeeId, preassignedConvId, invokedArea) {
|
|
7667
7686
|
if (!hubEmployeeIsAvailable(employeeId)) {
|
|
7668
7687
|
renderHubAgentSetupPanel();
|
|
@@ -7674,13 +7693,7 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
|
|
|
7674
7693
|
// In task-pane/extension mode: get a fresh selection snapshot and prepend
|
|
7675
7694
|
// document context to the instructions so the agent knows what the user is
|
|
7676
7695
|
// looking at. stubPath resolution and FRAIM invocation are now server-side.
|
|
7677
|
-
|
|
7678
|
-
if (document.body.dataset.surface === 'task-pane' || document.body.dataset.surface === 'extension') {
|
|
7679
|
-
const fresh = await requestWordContext('get-selection').catch(() => null);
|
|
7680
|
-
const wc = (fresh && (fresh.selection || fresh.hasSelection)) ? { ...state.wordContext, ...fresh } : state.wordContext;
|
|
7681
|
-
const ctxBlock = buildWordContextBlock(wc);
|
|
7682
|
-
if (ctxBlock) effectiveInstructions = ctxBlock + '\n\n---\n\n' + effectiveInstructions;
|
|
7683
|
-
}
|
|
7696
|
+
const effectiveInstructions = await withWordContext(instructions);
|
|
7684
7697
|
|
|
7685
7698
|
// Issue #442: read the A/B toggle state from the modal before it closes.
|
|
7686
7699
|
const abToggle = document.getElementById('ab-toggle');
|
|
@@ -7859,13 +7872,20 @@ async function continueRun(text, options) {
|
|
|
7859
7872
|
upsertConversation(conv);
|
|
7860
7873
|
refreshStatusSurfaces(); // #533 R5: also recolor the tree/area dots back to working
|
|
7861
7874
|
renderActive();
|
|
7875
|
+
// Issue #1239: task-pane/extension surfaces must attach current Word document
|
|
7876
|
+
// context to every follow-up message, the same way startRun() does for the
|
|
7877
|
+
// job-start instructions — otherwise a highlighted selection or a question
|
|
7878
|
+
// asked mid-conversation reaches the agent with zero document context. Runs
|
|
7879
|
+
// after the render above so the UI flips to "running" immediately on send,
|
|
7880
|
+
// not after the (up to few-hundred-ms) context round trip.
|
|
7881
|
+
const effectiveText = await withWordContext(text);
|
|
7862
7882
|
try {
|
|
7863
7883
|
let run;
|
|
7864
7884
|
try {
|
|
7865
7885
|
run = await requestJson(`/api/ai-hub/runs/${conv.runId}/messages`, {
|
|
7866
7886
|
method: 'POST',
|
|
7867
7887
|
headers: { 'Content-Type': 'application/json' },
|
|
7868
|
-
body: JSON.stringify({ instructions:
|
|
7888
|
+
body: JSON.stringify({ instructions: effectiveText, ...(coachingJobId ? { coachingJobId } : {}), ...(deliveryIntent ? { deliveryIntent } : {}) }),
|
|
7869
7889
|
});
|
|
7870
7890
|
} catch (e) {
|
|
7871
7891
|
const isNotFound = /not found/i.test((e && e.message) || '');
|
|
@@ -7888,7 +7908,7 @@ async function continueRun(text, options) {
|
|
|
7888
7908
|
conversationId: conv.id,
|
|
7889
7909
|
conversationTitle: conv.title,
|
|
7890
7910
|
sessionId: conv.sessionId,
|
|
7891
|
-
instructions:
|
|
7911
|
+
instructions: effectiveText,
|
|
7892
7912
|
...(coachingJobId ? { coachingJobId } : {}),
|
|
7893
7913
|
}),
|
|
7894
7914
|
});
|
|
@@ -7908,7 +7928,7 @@ async function continueRun(text, options) {
|
|
|
7908
7928
|
jobTitle: conv.jobTitle || conv.jobId,
|
|
7909
7929
|
conversationId: conv.id,
|
|
7910
7930
|
conversationTitle: conv.title,
|
|
7911
|
-
instructions:
|
|
7931
|
+
instructions: effectiveText,
|
|
7912
7932
|
...(coachingJobId ? { coachingJobId } : {}),
|
|
7913
7933
|
}),
|
|
7914
7934
|
});
|
|
@@ -8159,7 +8179,9 @@ async function tryWordWriteBack(conv) {
|
|
|
8159
8179
|
try {
|
|
8160
8180
|
await requestWordContext('track-changes-on');
|
|
8161
8181
|
const action = conv.wordStartedWithSelection ? 'insert-after' : 'append-to-doc';
|
|
8162
|
-
|
|
8182
|
+
// jobLabel (issue #1248): Word/PowerPoint ignore this extra field; Excel's
|
|
8183
|
+
// write-back audit comment (R4) uses it to name the job that wrote the cell.
|
|
8184
|
+
await requestWordContext(action, { text: outputText, jobLabel: conv.jobTitle || conv.jobId });
|
|
8163
8185
|
await requestWordContext('track-changes-off');
|
|
8164
8186
|
showStatus('Result written to document. Review tracked changes.');
|
|
8165
8187
|
} catch (e) {
|