fraim-framework 2.0.163 → 2.0.164
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/desktop-main.js +4 -1
- package/dist/src/ai-hub/hosts.js +4 -11
- package/dist/src/ai-hub/server.js +48 -123
- package/dist/src/cli/commands/init-project.js +15 -14
- package/dist/src/cli/commands/sync.js +38 -0
- package/dist/src/cli/utils/git-org-sync.js +56 -0
- package/dist/src/cli/utils/org-migration.js +50 -0
- package/dist/src/cli/utils/org-pack-sync.js +208 -0
- package/dist/src/cli/utils/project-bootstrap.js +20 -7
- package/dist/src/cli/utils/user-config.js +68 -0
- package/dist/src/core/fraim-config-schema.generated.js +10 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +78 -29
- package/dist/src/local-mcp-server/stdio-server.js +30 -0
- package/index.js +1 -1
- package/package.json +1 -2
- package/public/ai-hub/index.html +2 -2
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
- package/public/ai-hub/review.css +15 -15
- package/public/ai-hub/script.js +70 -78
- package/public/ai-hub/styles.css +173 -16
- package/public/first-run/styles.css +73 -73
- package/dist/src/ai-hub/word-sideload.js +0 -95
- package/dist/src/cli/commands/test-mcp.js +0 -171
- package/dist/src/cli/setup/first-run.js +0 -242
- package/dist/src/core/config-writer.js +0 -75
- package/dist/src/core/utils/job-aliases.js +0 -47
- package/dist/src/core/utils/workflow-parser.js +0 -174
package/public/ai-hub/script.js
CHANGED
|
@@ -539,10 +539,18 @@ function stripReviewHandoffBlocks(text) {
|
|
|
539
539
|
.trim();
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
function stripHubInjectedPromptBlocks(text) {
|
|
543
|
+
return String(text || '')
|
|
544
|
+
.replace(/(?:^|\n)\s*\[How to talk to me\][\s\S]*?(?=\n\s*\[[^\]\n]+\]|\n\s*#{1,6}\s|\n\s*[-*]\s|\n{2,}|$)/gi, '\n')
|
|
545
|
+
.replace(/(?:^|\n)\s*(?:[$/]fraim)\s+[a-z0-9-]+(?:\s*\[[^\]\n]*\])?[^\n]*(?=\n|$)/gi, '\n')
|
|
546
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
547
|
+
.trim();
|
|
548
|
+
}
|
|
549
|
+
|
|
542
550
|
// R8: render markdown subset safely. HTML is escaped first.
|
|
543
551
|
function formatEmployeeText(text) {
|
|
544
552
|
if (!text) return '';
|
|
545
|
-
const visibleText = stripReviewHandoffBlocks(text);
|
|
553
|
+
const visibleText = stripHubInjectedPromptBlocks(stripReviewHandoffBlocks(text));
|
|
546
554
|
if (!visibleText) return '';
|
|
547
555
|
// 1. Escape HTML entities.
|
|
548
556
|
let s = visibleText
|
|
@@ -2084,7 +2092,7 @@ function stripStubReference(text) {
|
|
|
2084
2092
|
}
|
|
2085
2093
|
|
|
2086
2094
|
function surfaceText(role, text, conv) {
|
|
2087
|
-
const raw = stripStubReference(text);
|
|
2095
|
+
const raw = stripHubInjectedPromptBlocks(stripStubReference(text));
|
|
2088
2096
|
if (!raw) return '';
|
|
2089
2097
|
|
|
2090
2098
|
if (role === 'manager') {
|
|
@@ -2207,20 +2215,13 @@ function ensureThreadMessageViewportObserver() {
|
|
|
2207
2215
|
}
|
|
2208
2216
|
|
|
2209
2217
|
function syncThreadMessageViewport() {
|
|
2210
|
-
const panel = els['thread-panel'];
|
|
2211
2218
|
const messages = els['messages'];
|
|
2212
|
-
if (!
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
messages.style.height = `${Math.max(120, available)}px`;
|
|
2219
|
-
messages.style.maxHeight = `${Math.max(120, available)}px`;
|
|
2220
|
-
} else {
|
|
2221
|
-
messages.style.height = '';
|
|
2222
|
-
messages.style.maxHeight = '';
|
|
2223
|
-
}
|
|
2219
|
+
if (!messages) return;
|
|
2220
|
+
// Flex layout now owns the thread height: #thread-panel[open] grows to fill the
|
|
2221
|
+
// available real estate and #messages (flex child, overflow-y:auto) scrolls
|
|
2222
|
+
// internally. Clear any legacy inline sizing so CSS stays authoritative.
|
|
2223
|
+
messages.style.height = '';
|
|
2224
|
+
messages.style.maxHeight = '';
|
|
2224
2225
|
}
|
|
2225
2226
|
|
|
2226
2227
|
function scrollThreadAfterViewportSync(conv, runningShouldStickToBottom, shouldScrollForUpdate) {
|
|
@@ -2249,7 +2250,8 @@ function scrollThreadForReview(conv) {
|
|
|
2249
2250
|
return;
|
|
2250
2251
|
}
|
|
2251
2252
|
|
|
2252
|
-
const
|
|
2253
|
+
const completion = host.querySelector('#review-completion');
|
|
2254
|
+
const target = completion || [...nodes].reverse().find((node) =>
|
|
2253
2255
|
node.classList.contains('employee') || node.classList.contains('system')
|
|
2254
2256
|
) || nodes[nodes.length - 1];
|
|
2255
2257
|
|
|
@@ -2257,7 +2259,7 @@ function scrollThreadForReview(conv) {
|
|
|
2257
2259
|
const targetRect = target.getBoundingClientRect();
|
|
2258
2260
|
const currentTop = host.scrollTop;
|
|
2259
2261
|
const targetTopInsideHost = currentTop + (targetRect.top - hostRect.top);
|
|
2260
|
-
const reviewOffset = Math.max(24, host.clientHeight * 0.16);
|
|
2262
|
+
const reviewOffset = completion ? 8 : Math.max(24, host.clientHeight * 0.16);
|
|
2261
2263
|
const desiredTop = Math.max(0, targetTopInsideHost - reviewOffset);
|
|
2262
2264
|
host.scrollTo({ top: desiredTop, behavior: 'smooth' });
|
|
2263
2265
|
}
|
|
@@ -2513,6 +2515,25 @@ function artifactLabel(artifact) {
|
|
|
2513
2515
|
return artifact.label || artifact.name || artifact.path || artifact.url || 'artifact';
|
|
2514
2516
|
}
|
|
2515
2517
|
|
|
2518
|
+
function artifactBasename(artifact) {
|
|
2519
|
+
const raw = artifact && (artifact.path || artifact.url || artifact.name || '');
|
|
2520
|
+
const cleaned = String(raw || '').split(/[?#]/)[0];
|
|
2521
|
+
return cleaned.split(/[\\/]/).filter(Boolean).pop() || '';
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
function artifactDisplayLabel(artifact) {
|
|
2525
|
+
const label = artifactLabel(artifact);
|
|
2526
|
+
const base = artifactBasename(artifact);
|
|
2527
|
+
if (!base || label === base || String(label).includes(base)) return label;
|
|
2528
|
+
return `${label} (${base})`;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function reviewArtifactSummary(handoff) {
|
|
2532
|
+
const artifacts = handoff && Array.isArray(handoff.artifacts) ? handoff.artifacts : [];
|
|
2533
|
+
if (!artifacts.length) return '';
|
|
2534
|
+
return artifacts.map(artifactDisplayLabel).join('; ');
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2516
2537
|
function artifactLocalPath(artifact) {
|
|
2517
2538
|
if (!artifact) return '';
|
|
2518
2539
|
const raw = artifact.path
|
|
@@ -2564,7 +2585,7 @@ function deriveFormatFromReviewHandoff(handoff) {
|
|
|
2564
2585
|
const actions = artifacts.length > 0
|
|
2565
2586
|
? artifacts.map((artifact, index) => ({
|
|
2566
2587
|
id: `review-artifact-${index}`,
|
|
2567
|
-
label:
|
|
2588
|
+
label: artifactDisplayLabel(artifact),
|
|
2568
2589
|
primary: index === 0,
|
|
2569
2590
|
kind: artifactActionKind(artifact),
|
|
2570
2591
|
artifact,
|
|
@@ -2628,7 +2649,7 @@ function deriveDeliverableFormat(conv) {
|
|
|
2628
2649
|
key: 'markdown',
|
|
2629
2650
|
label: artifactName || artifactLabel(artifact),
|
|
2630
2651
|
actions: [
|
|
2631
|
-
{ id: '
|
|
2652
|
+
{ id: 'open-artifact', label: artifactDisplayLabel(artifact), primary: true, kind: 'doc', artifact },
|
|
2632
2653
|
{ id: 'inline-feedback', label: 'Type feedback inline', primary: false, kind: 'feedback', artifact },
|
|
2633
2654
|
],
|
|
2634
2655
|
};
|
|
@@ -2649,7 +2670,7 @@ function deriveDeliverableFormat(conv) {
|
|
|
2649
2670
|
// read-side already gives us and degrade gracefully when a field is unknown.
|
|
2650
2671
|
function stripMarkdownForDisplay(text) {
|
|
2651
2672
|
if (!text) return text;
|
|
2652
|
-
return stripReviewHandoffBlocks(text)
|
|
2673
|
+
return stripHubInjectedPromptBlocks(stripReviewHandoffBlocks(text))
|
|
2653
2674
|
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
2654
2675
|
.replace(/\*(.+?)\*/g, '$1')
|
|
2655
2676
|
.replace(/^---+$/gm, '')
|
|
@@ -2678,8 +2699,10 @@ function buildReviewCardRows(conv, fmt) {
|
|
|
2678
2699
|
: `Completed "${conv.title || conv.jobTitle || 'the assigned work'}".`;
|
|
2679
2700
|
return [
|
|
2680
2701
|
{ k: 'What changed', v: whatChanged },
|
|
2681
|
-
|
|
2682
|
-
? { k: 'Files updated', v: `${fmt.label}` }
|
|
2702
|
+
handoff && handoff.reviewTarget && handoff.reviewTarget.type === 'artifact_set'
|
|
2703
|
+
? { k: 'Files updated', v: reviewArtifactSummary(handoff) || `${fmt.label}` }
|
|
2704
|
+
: reliableLabel
|
|
2705
|
+
? { k: 'Files updated', v: `${fmt.label}` }
|
|
2683
2706
|
: { k: 'Where to look', v: 'Open the updated files in your project (the Brief section shows the project context & rules) to review.' },
|
|
2684
2707
|
{ k: 'What to do next', v: 'Approve, or type what to change in the Coach box below.' },
|
|
2685
2708
|
];
|
|
@@ -2687,15 +2710,27 @@ function buildReviewCardRows(conv, fmt) {
|
|
|
2687
2710
|
const whatIDid = lastEmployee
|
|
2688
2711
|
? clampSummaryText(stripMarkdownForDisplay(lastEmployee), 220)
|
|
2689
2712
|
: `Completed “${conv.title || conv.jobTitle || 'the assigned work'}”.`;
|
|
2713
|
+
if (handoff && handoff.reviewTarget && handoff.reviewTarget.type === 'artifact_set') {
|
|
2714
|
+
return [
|
|
2715
|
+
{ k: 'Files to review', v: reviewArtifactSummary(handoff) || `${fmt.label}` },
|
|
2716
|
+
{ k: 'What I need', v: 'Your review — approve, or tell me what to sharpen.' },
|
|
2717
|
+
];
|
|
2718
|
+
}
|
|
2690
2719
|
const rows = [{ k: 'What I did', v: whatIDid }];
|
|
2691
|
-
if (reliableLabel)
|
|
2720
|
+
if (reliableLabel) {
|
|
2721
|
+
rows.push({ k: 'Deliverable', v: `${fmt.label}` });
|
|
2722
|
+
}
|
|
2692
2723
|
rows.push({ k: 'What I need', v: 'Your review — approve, or tell me what to sharpen.' });
|
|
2693
2724
|
return rows;
|
|
2694
2725
|
}
|
|
2695
2726
|
|
|
2696
|
-
// The completion card
|
|
2697
|
-
//
|
|
2698
|
-
//
|
|
2727
|
+
// The completion card is a decision surface, not chat history. Keep it outside
|
|
2728
|
+
// the collapsible thread so the exact deliverables remain visible even when the
|
|
2729
|
+
// manager minimizes the conversation transcript.
|
|
2730
|
+
// The "Ready for your review" card is the employee's review turn, so it lives as
|
|
2731
|
+
// the LAST child of #messages — inside the manager/employee thread. Keeping it in
|
|
2732
|
+
// the thread (rather than as a separate panel below it) means it scrolls with the
|
|
2733
|
+
// conversation and the thread does not need to collapse to surface it.
|
|
2699
2734
|
function reviewCompletionHost() {
|
|
2700
2735
|
const messages = els['messages'];
|
|
2701
2736
|
if (!messages) return null;
|
|
@@ -2713,6 +2748,12 @@ function clearReviewCompletion() {
|
|
|
2713
2748
|
const messages = els['messages'];
|
|
2714
2749
|
const existing = messages && messages.querySelector('#review-completion');
|
|
2715
2750
|
if (existing) existing.remove();
|
|
2751
|
+
if (messages) {
|
|
2752
|
+
messages.classList.remove('review-focused');
|
|
2753
|
+
messages.querySelectorAll('.review-source-message').forEach((node) => {
|
|
2754
|
+
node.classList.remove('review-source-message');
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2716
2757
|
}
|
|
2717
2758
|
|
|
2718
2759
|
function renderReviewExperience(conv) {
|
|
@@ -2792,9 +2833,9 @@ function renderReviewExperience(conv) {
|
|
|
2792
2833
|
}
|
|
2793
2834
|
}
|
|
2794
2835
|
|
|
2795
|
-
// Comments follow the artifact (R7.7): a PR opens GitHub;
|
|
2796
|
-
//
|
|
2797
|
-
// read-side already knows and otherwise leave a status hint.
|
|
2836
|
+
// Comments follow the artifact (R7.7): a PR opens GitHub; local artifacts open
|
|
2837
|
+
// from disk; reports view inline. Nothing new is invented - these surface what
|
|
2838
|
+
// the read-side already knows and otherwise leave a status hint.
|
|
2798
2839
|
async function handleArtifactAction(conv, action) {
|
|
2799
2840
|
const artifact = action.artifact || ((conv && conv.artifacts && conv.artifacts[0]) || null);
|
|
2800
2841
|
if (action.kind === 'github') {
|
|
@@ -2838,55 +2879,6 @@ async function handleArtifactAction(conv, action) {
|
|
|
2838
2879
|
}
|
|
2839
2880
|
return;
|
|
2840
2881
|
}
|
|
2841
|
-
if (action.kind === 'export') {
|
|
2842
|
-
// Download the deliverable as .docx for Word annotation. Try the on-disk file
|
|
2843
|
-
// first; if it doesn't actually exist (404 — e.g. the employee named a file it
|
|
2844
|
-
// never created) or there is no path, fall back to exporting the employee's
|
|
2845
|
-
// written deliverable text. Either way we download a real .docx, never a JSON
|
|
2846
|
-
// error. Feedback comes back through the Coach box (no "Done reviewing").
|
|
2847
|
-
const artifactPath = artifact && (artifact.path || artifact.where || artifact.url);
|
|
2848
|
-
const hasLocalFile = artifactPath && !String(artifactPath).startsWith('http');
|
|
2849
|
-
const baseName = ((artifact && artifact.name) || conversationTitle(conv) || 'deliverable');
|
|
2850
|
-
const triggerDownload = (blob) => {
|
|
2851
|
-
const url = URL.createObjectURL(blob);
|
|
2852
|
-
const a = document.createElement('a');
|
|
2853
|
-
a.href = url;
|
|
2854
|
-
a.download = String(baseName).replace(/\.[^.]+$/, '') + '.docx';
|
|
2855
|
-
a.style.display = 'none';
|
|
2856
|
-
document.body.appendChild(a);
|
|
2857
|
-
a.click();
|
|
2858
|
-
document.body.removeChild(a);
|
|
2859
|
-
URL.revokeObjectURL(url);
|
|
2860
|
-
};
|
|
2861
|
-
const isDocx = (resp) => (resp.headers.get('content-type') || '').includes('word');
|
|
2862
|
-
try {
|
|
2863
|
-
let blob = null;
|
|
2864
|
-
if (hasLocalFile) {
|
|
2865
|
-
const resp = await fetch('/api/ai-hub/artifact/export-docx?path=' + encodeURIComponent(artifactPath));
|
|
2866
|
-
if (resp.ok && isDocx(resp)) blob = await resp.blob();
|
|
2867
|
-
// else: file missing on disk → fall through to the inline export below.
|
|
2868
|
-
}
|
|
2869
|
-
if (!blob) {
|
|
2870
|
-
const inline = (typeof latestEmployeeSurfaceText === 'function' && latestEmployeeSurfaceText(conv)) || '';
|
|
2871
|
-
if (!inline.trim()) {
|
|
2872
|
-
showStatus('Nothing to download yet — the employee has not produced a file or a written deliverable.', true);
|
|
2873
|
-
return;
|
|
2874
|
-
}
|
|
2875
|
-
const resp = await fetch('/api/ai-hub/artifact/export-docx', {
|
|
2876
|
-
method: 'POST',
|
|
2877
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2878
|
-
body: JSON.stringify({ content: inline, filename: baseName }),
|
|
2879
|
-
});
|
|
2880
|
-
if (!resp.ok) { const e = await resp.json().catch(() => ({})); throw new Error(e.error || 'Export failed.'); }
|
|
2881
|
-
blob = await resp.blob();
|
|
2882
|
-
}
|
|
2883
|
-
triggerDownload(blob);
|
|
2884
|
-
showStatus('Downloaded as .docx — mark it up in Word, then type your feedback in the Coach box below to send changes back.', false);
|
|
2885
|
-
} catch (err) {
|
|
2886
|
-
showStatus(err instanceof Error ? err.message : 'Could not export the deliverable.', true);
|
|
2887
|
-
}
|
|
2888
|
-
return;
|
|
2889
|
-
}
|
|
2890
2882
|
// Any other action: feedback goes through the Coach box.
|
|
2891
2883
|
showStatus('Type your feedback in the Coach box below — it becomes a coaching message for the employee.', false);
|
|
2892
2884
|
}
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
color-scheme: light dark;
|
|
3
|
+
/* #ai-hub-polish: allow block-size:auto to animate (accordion open/close). */
|
|
4
|
+
interpolate-size: allow-keywords;
|
|
3
5
|
/* Light theme — Apple-style near-white */
|
|
4
6
|
--bg: #f5f5f7;
|
|
5
7
|
--surface: #ffffff;
|
|
@@ -855,6 +857,11 @@ img.conv-employee-avatar {
|
|
|
855
857
|
flex-shrink: 0;
|
|
856
858
|
}
|
|
857
859
|
.panel-details[open] > summary::before { transform: rotate(90deg); }
|
|
860
|
+
.panel-details:not([open]) > .panel-body,
|
|
861
|
+
.panel-details:not([open]) > .messages,
|
|
862
|
+
.panel-details:not([open]) > .micro-log {
|
|
863
|
+
display: none;
|
|
864
|
+
}
|
|
858
865
|
.panel-summary-copy {
|
|
859
866
|
display: flex;
|
|
860
867
|
flex-direction: column;
|
|
@@ -1314,13 +1321,18 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
|
|
|
1314
1321
|
|
|
1315
1322
|
.micro {
|
|
1316
1323
|
margin-top: 0;
|
|
1324
|
+
border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
|
|
1325
|
+
background: color-mix(in srgb, var(--accent-soft) 48%, var(--soft));
|
|
1317
1326
|
/* Removed: position:sticky / bottom:0 / z-index:2 — sticky caused the
|
|
1318
1327
|
micro-manage label to float over the coach section in the flex column.
|
|
1319
1328
|
It's now a normal flow element at the bottom of the support-stack. */
|
|
1320
1329
|
}
|
|
1330
|
+
.micro .panel-kicker {
|
|
1331
|
+
color: var(--accent-strong);
|
|
1332
|
+
}
|
|
1321
1333
|
.micro summary {
|
|
1322
1334
|
cursor: pointer;
|
|
1323
|
-
color: var(--
|
|
1335
|
+
color: var(--text);
|
|
1324
1336
|
font-size: 14px;
|
|
1325
1337
|
list-style: none;
|
|
1326
1338
|
display: flex;
|
|
@@ -2404,7 +2416,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2404
2416
|
.syn-row { display: flex; align-items: flex-start; gap: 14px; padding: 12px 18px; border-bottom: 1px solid var(--line); }
|
|
2405
2417
|
.syn-row:last-child { border-bottom: none; }
|
|
2406
2418
|
.syn-label { font-size: 12px; font-weight: 600; color: var(--muted); width: 90px; flex-shrink: 0; padding-top: 1px; }
|
|
2407
|
-
.syn-val { flex: 1; font-size: 13px; color: var(--text); line-height: 1.5; }
|
|
2419
|
+
.syn-val { flex: 1; min-width: 0; overflow-wrap: break-word; font-size: 13px; color: var(--text); line-height: 1.5; }
|
|
2408
2420
|
.syn-edit { font-size: 12px; color: var(--accent); font-weight: 500; background: none; border: none; cursor: pointer; flex-shrink: 0; }
|
|
2409
2421
|
|
|
2410
2422
|
/* Learnings */
|
|
@@ -2467,7 +2479,22 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2467
2479
|
.ctx-acc > summary .ca-chev { font-size: 10px; color: var(--muted); transition: transform .15s; }
|
|
2468
2480
|
.ctx-acc[open] > summary .ca-chev { transform: rotate(90deg); }
|
|
2469
2481
|
.ctx-acc > summary .ca-note { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 11px; color: var(--muted); }
|
|
2470
|
-
|
|
2482
|
+
/* #ai-hub-polish F2: a long section must not bury the others. Cap the body and
|
|
2483
|
+
let it scroll internally so every accordion header stays reachable. */
|
|
2484
|
+
.ctx-acc-body { padding: 4px 14px 14px; max-height: min(62vh, 620px); overflow-y: auto; }
|
|
2485
|
+
|
|
2486
|
+
/* #ai-hub-polish F1: ease the open/close instead of snapping. Animate the
|
|
2487
|
+
<details> content slot; interpolate-size lets block-size animate to/from auto.
|
|
2488
|
+
@supports keeps older engines on the instant (pre-polish) behavior, and the
|
|
2489
|
+
prefers-reduced-motion block above neutralizes the transition. */
|
|
2490
|
+
@supports selector(::details-content) {
|
|
2491
|
+
.ctx-acc::details-content {
|
|
2492
|
+
block-size: 0;
|
|
2493
|
+
overflow: clip;
|
|
2494
|
+
transition: block-size .24s ease, content-visibility .24s allow-discrete;
|
|
2495
|
+
}
|
|
2496
|
+
.ctx-acc[open]::details-content { block-size: auto; }
|
|
2497
|
+
}
|
|
2471
2498
|
|
|
2472
2499
|
/* #521: project-onboarding loop banner inside the Brief — "team is processing /
|
|
2473
2500
|
here's their understanding to review". */
|
|
@@ -2639,26 +2666,115 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2639
2666
|
padding: 16px 18px;
|
|
2640
2667
|
}
|
|
2641
2668
|
|
|
2642
|
-
/* Conversation view (full height):
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2669
|
+
/* Conversation view (full height): the open panels SHARE the available real
|
|
2670
|
+
estate instead of being pinned to fixed caps. Each open panel grows to fill its
|
|
2671
|
+
slice and scrolls internally when its content overflows (its own scrollbar).
|
|
2672
|
+
#active-conv keeps overflow-y:auto only as a fallback for very short windows
|
|
2673
|
+
where the panels' min-heights can't all fit. */
|
|
2646
2674
|
.workspace-conv #active-conv {
|
|
2675
|
+
overflow-y: auto;
|
|
2676
|
+
display: flex;
|
|
2677
|
+
flex-direction: column;
|
|
2678
|
+
gap: 8px;
|
|
2679
|
+
min-height: 0;
|
|
2680
|
+
}
|
|
2681
|
+
.workspace-conv #active-conv > .conv-topline,
|
|
2682
|
+
.workspace-conv #active-conv > .conversation-status { flex: 0 0 auto; }
|
|
2683
|
+
|
|
2684
|
+
/* Manager/employee thread — the primary conversation. The "Ready for review"
|
|
2685
|
+
card lives inside #messages, so it scrolls with the thread. Open thread grows
|
|
2686
|
+
to fill spare height; collapsed it is just its summary bar. */
|
|
2687
|
+
.workspace-conv #thread-panel { flex: 0 0 auto; min-height: 0; }
|
|
2688
|
+
/* Open thread is the sole space-filler: it grows to take all height the support
|
|
2689
|
+
stack does not need, and scrolls #messages internally. Collapsed it is just
|
|
2690
|
+
its summary bar. */
|
|
2691
|
+
.workspace-conv #thread-panel[open] {
|
|
2692
|
+
flex: 1 1 0;
|
|
2693
|
+
display: flex;
|
|
2694
|
+
flex-direction: column;
|
|
2695
|
+
min-height: 120px;
|
|
2696
|
+
}
|
|
2697
|
+
.workspace-conv #thread-panel > summary { flex: 0 0 auto; }
|
|
2698
|
+
/* Chromium wraps <details> content in a ::details-content slot, so #messages is
|
|
2699
|
+
not a direct flex child of the panel. Make the slot a column flex container
|
|
2700
|
+
that fills the panel; then #messages can flex-grow and scroll inside it. */
|
|
2701
|
+
.workspace-conv #thread-panel[open]::details-content {
|
|
2702
|
+
flex: 1 1 0;
|
|
2703
|
+
min-height: 0;
|
|
2704
|
+
display: flex;
|
|
2705
|
+
flex-direction: column;
|
|
2647
2706
|
overflow: hidden;
|
|
2648
|
-
display: grid;
|
|
2649
|
-
grid-template-rows: auto auto minmax(200px, 1fr) auto;
|
|
2650
2707
|
}
|
|
2651
|
-
.workspace-conv #thread-panel[open] {
|
|
2652
|
-
|
|
2653
|
-
|
|
2708
|
+
.workspace-conv #thread-panel[open] #messages {
|
|
2709
|
+
flex: 1 1 0;
|
|
2710
|
+
min-height: 0;
|
|
2711
|
+
max-height: none;
|
|
2712
|
+
overflow-y: auto;
|
|
2713
|
+
}
|
|
2714
|
+
.workspace-conv #thread-panel:not([open]) #messages { display: none; }
|
|
2715
|
+
|
|
2716
|
+
/* Support stack: Coach (input) + Micro-manage (log). It sizes to its content and
|
|
2717
|
+
never grows past it, so the thread keeps the spare real estate. Each open body
|
|
2718
|
+
has a viewport-relative cap and scrolls internally, so it shows more on bigger
|
|
2719
|
+
screens (and far more than the old 1-line peek) without crowding the thread. */
|
|
2654
2720
|
.workspace-conv .support-stack {
|
|
2655
|
-
display:
|
|
2721
|
+
display: flex;
|
|
2722
|
+
flex-direction: column;
|
|
2723
|
+
gap: 8px;
|
|
2656
2724
|
min-height: 0;
|
|
2657
|
-
|
|
2725
|
+
flex: 0 1 auto;
|
|
2726
|
+
}
|
|
2727
|
+
.workspace-conv .support-stack > .panel-details--coach { flex: 0 0 auto; }
|
|
2728
|
+
.workspace-conv .support-stack > .panel-details--coach[open] > .panel-body {
|
|
2729
|
+
max-height: clamp(140px, 24vh, 260px);
|
|
2730
|
+
overflow-y: auto;
|
|
2731
|
+
}
|
|
2732
|
+
.workspace-conv .panel-details--coach .panel-body { padding-bottom: 8px; }
|
|
2733
|
+
.workspace-conv .coach-input textarea {
|
|
2734
|
+
min-height: 44px;
|
|
2735
|
+
padding-top: 9px;
|
|
2736
|
+
padding-bottom: 9px;
|
|
2737
|
+
}
|
|
2738
|
+
.workspace-conv .coach-note { display: none; }
|
|
2739
|
+
.workspace-conv .quick-coach-row { gap: 5px; margin-bottom: 8px; }
|
|
2740
|
+
.workspace-conv .quick-coach-btn { padding: 4px 9px; }
|
|
2741
|
+
|
|
2742
|
+
.workspace-conv .support-stack > .micro { flex: 0 0 auto; min-height: 0; }
|
|
2743
|
+
.workspace-conv .support-stack > .micro[open] > .micro-log {
|
|
2744
|
+
max-height: clamp(120px, 24vh, 260px);
|
|
2658
2745
|
overflow-y: auto;
|
|
2659
2746
|
}
|
|
2660
2747
|
.workspace-conv .support-stack > .panel-details,
|
|
2661
|
-
.workspace-conv .support-stack > .micro { margin-bottom:
|
|
2748
|
+
.workspace-conv .support-stack > .micro { margin-bottom: 0; }
|
|
2749
|
+
|
|
2750
|
+
@media (max-width: 820px) {
|
|
2751
|
+
/* On narrow screens drop the fill model: panels flow naturally and the page
|
|
2752
|
+
scrolls, with each open body keeping a viewport-relative scroll cap. */
|
|
2753
|
+
.workspace-conv #active-conv {
|
|
2754
|
+
display: flex;
|
|
2755
|
+
flex-direction: column;
|
|
2756
|
+
overflow: visible;
|
|
2757
|
+
}
|
|
2758
|
+
.workspace-conv #thread-panel[open],
|
|
2759
|
+
.workspace-conv .support-stack,
|
|
2760
|
+
.workspace-conv .support-stack:has(.micro[open]),
|
|
2761
|
+
.workspace-conv .support-stack > .micro[open] {
|
|
2762
|
+
flex: 0 0 auto;
|
|
2763
|
+
display: block;
|
|
2764
|
+
min-height: 0;
|
|
2765
|
+
}
|
|
2766
|
+
.workspace-conv #thread-panel[open] #messages,
|
|
2767
|
+
.workspace-conv .support-stack > .micro[open] > .micro-log {
|
|
2768
|
+
flex: initial;
|
|
2769
|
+
height: auto;
|
|
2770
|
+
max-height: clamp(140px, 32vh, 280px);
|
|
2771
|
+
overflow-y: auto;
|
|
2772
|
+
}
|
|
2773
|
+
.workspace-conv .support-stack > .panel-details[open] > .panel-body {
|
|
2774
|
+
max-height: clamp(140px, 32vh, 280px);
|
|
2775
|
+
overflow-y: auto;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2662
2778
|
|
|
2663
2779
|
@media (max-width: 640px) {
|
|
2664
2780
|
body.hub-shell { overflow-x: hidden; }
|
|
@@ -2885,6 +3001,39 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2885
3001
|
.workspace-conv > .page { overflow: hidden; }
|
|
2886
3002
|
.workspace-conv .layout { display: flex; gap: 0; min-height: 0; flex: 1; overflow: hidden; }
|
|
2887
3003
|
|
|
3004
|
+
@media (max-width: 820px) {
|
|
3005
|
+
body.hub-shell {
|
|
3006
|
+
height: auto;
|
|
3007
|
+
min-height: 100vh;
|
|
3008
|
+
overflow: auto;
|
|
3009
|
+
}
|
|
3010
|
+
.hub-area,
|
|
3011
|
+
.area-main,
|
|
3012
|
+
#proj-workspace,
|
|
3013
|
+
.workspace-conv,
|
|
3014
|
+
.workspace-conv > .page,
|
|
3015
|
+
.workspace-conv .layout {
|
|
3016
|
+
overflow: visible;
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
/* #ai-hub-polish F3: Company & Manager areas had no narrow-width layout — the
|
|
3021
|
+
fixed 244px rail pushed the main column off the right edge (horizontal scroll).
|
|
3022
|
+
Below the Projects-workspace breakpoint, stack the rail above the content and
|
|
3023
|
+
let the page shrink so nothing overflows. 2-column stays intact on tablets. */
|
|
3024
|
+
@media (max-width: 640px) {
|
|
3025
|
+
body.hub-shell { overflow-x: hidden; }
|
|
3026
|
+
.area-shell { flex-direction: column; }
|
|
3027
|
+
.area-rail {
|
|
3028
|
+
width: 100%;
|
|
3029
|
+
flex-shrink: 0;
|
|
3030
|
+
max-height: 200px;
|
|
3031
|
+
border-right: none;
|
|
3032
|
+
border-bottom: 1px solid var(--line);
|
|
3033
|
+
}
|
|
3034
|
+
.hub-area-page { padding: 22px 16px 40px; min-width: 0; max-width: 100%; }
|
|
3035
|
+
}
|
|
3036
|
+
|
|
2888
3037
|
|
|
2889
3038
|
/* ── #512 Round 2: actual class names from rendered HTML ────────────────── */
|
|
2890
3039
|
|
|
@@ -2899,8 +3048,16 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
2899
3048
|
.syn-row { display: flex; align-items: flex-start; gap: 14px; padding: 13px 18px; border-bottom: 1px solid var(--line); }
|
|
2900
3049
|
.syn-row:last-child { border-bottom: none; }
|
|
2901
3050
|
.syn-label { font-size: 12px; font-weight: 600; color: var(--muted); width: 88px; flex-shrink: 0; padding-top: 2px; }
|
|
2902
|
-
.syn-val { flex: 1; font-size: 13px; color: var(--text); line-height: 1.5; }
|
|
3051
|
+
.syn-val { flex: 1; min-width: 0; overflow-wrap: break-word; font-size: 13px; color: var(--text); line-height: 1.5; }
|
|
2903
3052
|
.syn-edit { font-size: 12px; color: var(--accent); font-weight: 500; background: none; border: none; cursor: pointer; flex-shrink: 0; }
|
|
3053
|
+
/* #ai-hub-polish: at phone widths the fixed label column starves the value and
|
|
3054
|
+
forces mid-word wraps. Stack label over value so the value gets full width.
|
|
3055
|
+
Placed after the duplicate .syn-* block above so source order lets it win. */
|
|
3056
|
+
@media (max-width: 560px) {
|
|
3057
|
+
.syn-row { flex-wrap: wrap; gap: 4px 12px; }
|
|
3058
|
+
.syn-label { width: 100%; }
|
|
3059
|
+
.syn-val { flex: 1 1 100%; }
|
|
3060
|
+
}
|
|
2904
3061
|
|
|
2905
3062
|
/* Issue #512 R3/R8 — Team Context inline editor. */
|
|
2906
3063
|
/* ctx-content is now a div with rendered markdown (formatEmployeeText), not a raw <pre> */
|