fraim-framework 2.0.152 → 2.0.154
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 +20 -4
- package/dist/src/ai-hub/server.js +40 -16
- package/dist/src/cli/commands/init-project.js +4 -2
- package/dist/src/cli/setup/ide-detector.js +46 -18
- package/dist/src/cli/utils/managed-agent-paths.js +48 -0
- package/dist/src/first-run/session-service.js +35 -48
- package/package.json +4 -2
- package/public/ai-hub/index.html +187 -179
- package/public/ai-hub/script.js +100 -45
- package/public/ai-hub/styles.css +112 -51
package/public/ai-hub/script.js
CHANGED
|
@@ -21,9 +21,10 @@ const state = {
|
|
|
21
21
|
selectedJob: null, // chosen in modal step 1
|
|
22
22
|
selectedEmployeeId: null,
|
|
23
23
|
selectedPersonaKey: null, // R4: null = "All employees"
|
|
24
|
-
modalPersonaFilter: null, // R3+: filter inside new-job modal; null = "All jobs"
|
|
25
|
-
storedApiKey: null, // R2: loaded from preferences, sent on bootstrap
|
|
26
|
-
}
|
|
24
|
+
modalPersonaFilter: null, // R3+: filter inside new-job modal; null = "All jobs"
|
|
25
|
+
storedApiKey: null, // R2: loaded from preferences, sent on bootstrap
|
|
26
|
+
panelState: {}, // { [convId]: { coach?: boolean } }
|
|
27
|
+
};
|
|
27
28
|
|
|
28
29
|
const els = {};
|
|
29
30
|
|
|
@@ -37,11 +38,12 @@ function gatherElements() {
|
|
|
37
38
|
'new-conv-btn', 'conv-list',
|
|
38
39
|
// Issue #385: team roster
|
|
39
40
|
'team-roster',
|
|
40
|
-
'empty', 'active-conv', 'active-title', 'active-job', 'active-identity', 'run-state-pill',
|
|
41
|
+
'empty', 'active-conv', 'active-title', 'active-job', 'active-identity', 'run-state-pill',
|
|
41
42
|
'progress', 'stage', 'latest', 'artifact-slot', 'messages',
|
|
42
43
|
'coach-text', 'send', 'micro-manage', 'micro-log',
|
|
43
44
|
'status-line', 'coach-note',
|
|
44
|
-
'
|
|
45
|
+
'coach-panel', 'coach-summary',
|
|
46
|
+
'modal', 'step1', 'step2',
|
|
45
47
|
'cancel1', 'next1', 'back2', 'start',
|
|
46
48
|
'job-search', 'job-catalog', 'job-pick-status',
|
|
47
49
|
// Issue #385: hire-required notice, persona job filter
|
|
@@ -224,10 +226,34 @@ function findConversation(id) {
|
|
|
224
226
|
return null;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
function activeConversation() {
|
|
228
|
-
if (!state.activeId) return null;
|
|
229
|
-
return findConversation(state.activeId);
|
|
230
|
-
}
|
|
229
|
+
function activeConversation() {
|
|
230
|
+
if (!state.activeId) return null;
|
|
231
|
+
return findConversation(state.activeId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function panelStateFor(convId) {
|
|
235
|
+
if (!convId) return {};
|
|
236
|
+
if (!state.panelState[convId]) state.panelState[convId] = {};
|
|
237
|
+
return state.panelState[convId];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function defaultCoachOpen(conv) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function syncConversationPanels(conv, switchedConv) {
|
|
245
|
+
const coach = els['coach-panel'];
|
|
246
|
+
if (!conv || !coach) return;
|
|
247
|
+
const panelState = panelStateFor(conv.id);
|
|
248
|
+
if (switchedConv) {
|
|
249
|
+
coach.open = panelState.coach ?? defaultCoachOpen(conv);
|
|
250
|
+
}
|
|
251
|
+
if (els['coach-summary']) {
|
|
252
|
+
els['coach-summary'].textContent = conv.status === 'running'
|
|
253
|
+
? 'Open to coach or redirect the work.'
|
|
254
|
+
: 'Open when you want to steer the next step.';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
231
257
|
|
|
232
258
|
function upsertConversation(conv) {
|
|
233
259
|
const list = projectConversations().slice();
|
|
@@ -565,7 +591,6 @@ function renderActive() {
|
|
|
565
591
|
renderConversationIdentity(conv);
|
|
566
592
|
els['active-job'].textContent = `${conv.jobTitle} · ${friendlyProjectName(conv.projectPath || state.projectPath)}`;
|
|
567
593
|
renderRunStatePill(conv);
|
|
568
|
-
els['summary-strip'].textContent = buildConversationSummary(conv);
|
|
569
594
|
els['coach-note'].textContent = conv.status === 'running'
|
|
570
595
|
? 'The employee is still working. Add coaching here to tighten the next step without losing context.'
|
|
571
596
|
: 'The employee is waiting on you. Send the next instruction to continue this run.';
|
|
@@ -579,7 +604,7 @@ function renderActive() {
|
|
|
579
604
|
|
|
580
605
|
// If we switched conversations (or this is the first render), wipe and
|
|
581
606
|
// start fresh. Otherwise we're going to do an incremental update below.
|
|
582
|
-
const switchedConv = renderedConvId !== conv.id;
|
|
607
|
+
const switchedConv = renderedConvId !== conv.id;
|
|
583
608
|
if (switchedConv) {
|
|
584
609
|
els['artifact-slot'].innerHTML = '';
|
|
585
610
|
els['messages'].innerHTML = '';
|
|
@@ -590,6 +615,7 @@ function renderActive() {
|
|
|
590
615
|
renderedArtifactKey = null;
|
|
591
616
|
}
|
|
592
617
|
const statusChanged = renderedStatus !== conv.status;
|
|
618
|
+
syncConversationPanels(conv, switchedConv);
|
|
593
619
|
|
|
594
620
|
// Artifact callout — only re-render when the latest artifact actually
|
|
595
621
|
// changed. Avoids the 'pulse' animation re-firing on every poll tick.
|
|
@@ -1025,21 +1051,23 @@ function closeTemplatePopover() {
|
|
|
1025
1051
|
btn.setAttribute('aria-expanded', 'false');
|
|
1026
1052
|
}
|
|
1027
1053
|
|
|
1028
|
-
function applyTemplateInvocation(managerJobId) {
|
|
1029
|
-
const conv = activeConversation();
|
|
1054
|
+
function applyTemplateInvocation(managerJobId) {
|
|
1055
|
+
const conv = activeConversation();
|
|
1030
1056
|
// Use the conversation's own employee for the invocation symbol, NOT
|
|
1031
1057
|
// the manager's last selection in another conversation (R2.5).
|
|
1032
1058
|
const employeeId = (conv && conv.employeeId) || state.selectedEmployeeId || 'claude';
|
|
1033
1059
|
const symbol = FRAIM_INVOCATION_SYMBOL[employeeId] || '/fraim';
|
|
1034
|
-
const invocation = `${symbol} ${managerJobId}`;
|
|
1035
|
-
const textarea = els['coach-text'];
|
|
1036
|
-
const prior = textarea.value;
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
else
|
|
1041
|
-
|
|
1042
|
-
|
|
1060
|
+
const invocation = `${symbol} ${managerJobId}`;
|
|
1061
|
+
const textarea = els['coach-text'];
|
|
1062
|
+
const prior = textarea.value;
|
|
1063
|
+
let combined;
|
|
1064
|
+
if (prior.trim().length === 0) {
|
|
1065
|
+
combined = invocation;
|
|
1066
|
+
} else {
|
|
1067
|
+
const strippedPrior = prior.replace(/(?:^|\n|\s)[/$]fraim\s+[a-z0-9-]+(?:\s|$)/ig, ' ').replace(/\s+/g, ' ').trim();
|
|
1068
|
+
combined = strippedPrior ? `${invocation}\n\n${strippedPrior}` : invocation;
|
|
1069
|
+
}
|
|
1070
|
+
textarea.value = combined;
|
|
1043
1071
|
// Caret at the end.
|
|
1044
1072
|
textarea.setSelectionRange(combined.length, combined.length);
|
|
1045
1073
|
textarea.focus();
|
|
@@ -1100,9 +1128,32 @@ function surfaceText(role, text, conv) {
|
|
|
1100
1128
|
}
|
|
1101
1129
|
}
|
|
1102
1130
|
|
|
1131
|
+
if (role === 'employee') {
|
|
1132
|
+
const resumedMatch = raw.match(/^Resumed\s+\w+\s+session\s+[a-f0-9-]+:\s*(.*)$/i);
|
|
1133
|
+
if (resumedMatch) {
|
|
1134
|
+
const cleaned = surfaceText('manager', resumedMatch[1], conv);
|
|
1135
|
+
if (conv.status === 'completed') return 'Done - please review.';
|
|
1136
|
+
return cleaned ? `Working on: ${cleaned}` : 'Working on it...';
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1103
1140
|
return raw;
|
|
1104
1141
|
}
|
|
1105
1142
|
|
|
1143
|
+
function extractExplicitFraimInvocation(text) {
|
|
1144
|
+
const raw = String(text || '');
|
|
1145
|
+
const match = raw.match(/(?:^|\n|\s)([$/]fraim)\s+([a-z0-9][a-z0-9-]*)(?=\s|$)/i);
|
|
1146
|
+
if (!match || match.index == null) return null;
|
|
1147
|
+
const before = raw.slice(0, match.index).trim();
|
|
1148
|
+
const after = raw.slice(match.index + match[0].length).trim();
|
|
1149
|
+
const remainder = [before, after].filter(Boolean).join('\n\n').trim();
|
|
1150
|
+
return {
|
|
1151
|
+
symbol: match[1],
|
|
1152
|
+
jobId: match[2],
|
|
1153
|
+
remainder,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1106
1157
|
function latestEmployeeSurfaceText(conv) {
|
|
1107
1158
|
const messages = conv.messages || [];
|
|
1108
1159
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -1664,22 +1715,18 @@ function fraimInvocationFor(employeeId, jobId, kind) {
|
|
|
1664
1715
|
// AND what we show in the timeline so the manager sees what the agent
|
|
1665
1716
|
// received. For freeform jobs (no FRAIM job assigned), the instructions
|
|
1666
1717
|
// are sent verbatim with no invocation prefix.
|
|
1667
|
-
function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
1668
|
-
const trimmed = (instructions || '').trim();
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
if (!
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (knownSymbols.some((s) => trimmed.startsWith(s))) return trimmed;
|
|
1680
|
-
}
|
|
1681
|
-
return `${invocation}${stub}\n\n${trimmed}`;
|
|
1682
|
-
}
|
|
1718
|
+
function buildAgentMessage(employeeId, jobId, kind, instructions, stubPath) {
|
|
1719
|
+
const trimmed = (instructions || '').trim();
|
|
1720
|
+
const explicit = extractExplicitFraimInvocation(trimmed);
|
|
1721
|
+
const effectiveJobId = explicit?.jobId || jobId;
|
|
1722
|
+
const invocation = fraimInvocationFor(employeeId, effectiveJobId, kind);
|
|
1723
|
+
// Freeform: no FRAIM prefix, no stub reference — just the raw instructions.
|
|
1724
|
+
if (!invocation) return explicit?.remainder || trimmed;
|
|
1725
|
+
const remainder = explicit ? explicit.remainder : trimmed;
|
|
1726
|
+
const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
|
|
1727
|
+
if (!remainder) return `${invocation}${stub}`;
|
|
1728
|
+
return `${invocation}${stub}\n\n${remainder}`;
|
|
1729
|
+
}
|
|
1683
1730
|
|
|
1684
1731
|
async function startRun(job, instructions, employeeId) {
|
|
1685
1732
|
// Prefix the manager's typed instructions with the FRAIM invocation so
|
|
@@ -2021,17 +2068,25 @@ function wireEvents() {
|
|
|
2021
2068
|
}
|
|
2022
2069
|
});
|
|
2023
2070
|
|
|
2024
|
-
if (els['active-employee-select']) {
|
|
2025
|
-
els['active-employee-select'].addEventListener('change', () => {
|
|
2071
|
+
if (els['active-employee-select']) {
|
|
2072
|
+
els['active-employee-select'].addEventListener('change', () => {
|
|
2026
2073
|
// Only update the global preference here. Do NOT update conv.employeeId —
|
|
2027
2074
|
// the send handler compares sel.value vs conv.employeeId to detect a
|
|
2028
2075
|
// switch; updating conv here would make them equal and the restart would
|
|
2029
2076
|
// never fire.
|
|
2030
|
-
state.selectedEmployeeId = els['active-employee-select'].value;
|
|
2031
|
-
});
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2077
|
+
state.selectedEmployeeId = els['active-employee-select'].value;
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
if (els['coach-panel']) {
|
|
2082
|
+
els['coach-panel'].addEventListener('toggle', () => {
|
|
2083
|
+
const conv = activeConversation();
|
|
2084
|
+
if (!conv) return;
|
|
2085
|
+
panelStateFor(conv.id).coach = els['coach-panel'].open;
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Issue #347 R2 — template picker.
|
|
2035
2090
|
if (els['template-picker-btn']) {
|
|
2036
2091
|
els['template-picker-btn'].addEventListener('click', (e) => {
|
|
2037
2092
|
e.stopPropagation();
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -388,9 +388,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
.conv-header,
|
|
391
|
-
.
|
|
392
|
-
.
|
|
393
|
-
.
|
|
391
|
+
.support-stack,
|
|
392
|
+
.panel-details,
|
|
393
|
+
.micro {
|
|
394
394
|
flex-shrink: 0;
|
|
395
395
|
}
|
|
396
396
|
.conv-topline {
|
|
@@ -482,24 +482,76 @@ button { font: inherit; cursor: pointer; }
|
|
|
482
482
|
|
|
483
483
|
.conv-header h2 { margin: 0; font-size: 34px; font-weight: 700; letter-spacing: -0.03em; }
|
|
484
484
|
.conv-job { color: var(--muted); font-size: 14px; margin-top: 2px; }
|
|
485
|
-
.
|
|
486
|
-
|
|
485
|
+
.conversation-status {
|
|
486
|
+
display: flex;
|
|
487
|
+
align-items: flex-start;
|
|
488
|
+
gap: 12px;
|
|
489
|
+
min-width: 0;
|
|
490
|
+
flex-shrink: 0;
|
|
491
|
+
}
|
|
492
|
+
.conversation-status > * {
|
|
493
|
+
min-width: 0;
|
|
494
|
+
}
|
|
495
|
+
.status-stack {
|
|
496
|
+
display: grid;
|
|
497
|
+
gap: 10px;
|
|
498
|
+
min-width: 0;
|
|
499
|
+
flex: 1 1 auto;
|
|
500
|
+
}
|
|
501
|
+
.support-stack {
|
|
502
|
+
display: grid;
|
|
503
|
+
gap: 8px;
|
|
504
|
+
}
|
|
505
|
+
.panel-details {
|
|
506
|
+
background: rgba(255, 255, 255, 0.4);
|
|
487
507
|
border: 1px solid rgba(31, 67, 125, 0.08);
|
|
488
|
-
border-radius:
|
|
489
|
-
|
|
508
|
+
border-radius: 22px;
|
|
509
|
+
overflow: hidden;
|
|
510
|
+
}
|
|
511
|
+
.panel-details > summary {
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
list-style: none;
|
|
514
|
+
display: flex;
|
|
515
|
+
align-items: center;
|
|
516
|
+
gap: 10px;
|
|
517
|
+
padding: 10px 14px;
|
|
518
|
+
}
|
|
519
|
+
.panel-details > summary::-webkit-details-marker { display: none; }
|
|
520
|
+
.panel-details > summary::before {
|
|
521
|
+
content: "▸";
|
|
522
|
+
font-size: 11px;
|
|
523
|
+
color: var(--muted);
|
|
524
|
+
transition: transform 100ms ease;
|
|
525
|
+
flex-shrink: 0;
|
|
526
|
+
}
|
|
527
|
+
.panel-details[open] > summary::before { transform: rotate(90deg); }
|
|
528
|
+
.panel-summary-copy {
|
|
529
|
+
display: flex;
|
|
530
|
+
flex-direction: column;
|
|
531
|
+
gap: 2px;
|
|
532
|
+
min-width: 0;
|
|
533
|
+
}
|
|
534
|
+
.panel-kicker {
|
|
535
|
+
font-size: 11px;
|
|
536
|
+
font-weight: 700;
|
|
537
|
+
letter-spacing: 0.12em;
|
|
538
|
+
text-transform: uppercase;
|
|
539
|
+
color: var(--muted);
|
|
540
|
+
}
|
|
541
|
+
.panel-summary-text {
|
|
542
|
+
font-size: 13px;
|
|
490
543
|
color: var(--text);
|
|
491
|
-
|
|
492
|
-
line-height: 1.55;
|
|
493
|
-
display: -webkit-box;
|
|
494
|
-
-webkit-line-clamp: 4;
|
|
495
|
-
-webkit-box-orient: vertical;
|
|
544
|
+
white-space: nowrap;
|
|
496
545
|
overflow: hidden;
|
|
546
|
+
text-overflow: ellipsis;
|
|
497
547
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
548
|
+
.panel-body {
|
|
549
|
+
padding: 0 14px 12px;
|
|
550
|
+
}
|
|
551
|
+
.section-title {
|
|
552
|
+
font-size: 13px;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: var(--muted);
|
|
503
555
|
text-transform: uppercase;
|
|
504
556
|
letter-spacing: 0.05em;
|
|
505
557
|
margin-bottom: 8px;
|
|
@@ -571,6 +623,15 @@ button { font: inherit; cursor: pointer; }
|
|
|
571
623
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
572
624
|
50% { opacity: 0.5; transform: scale(0.8); }
|
|
573
625
|
}
|
|
626
|
+
.progress-inline {
|
|
627
|
+
flex: 1 1 auto;
|
|
628
|
+
}
|
|
629
|
+
.tracker-inline {
|
|
630
|
+
background: rgba(255, 255, 255, 0.42);
|
|
631
|
+
border: 1px solid rgba(31, 67, 125, 0.08);
|
|
632
|
+
border-radius: 18px;
|
|
633
|
+
padding: 8px 12px;
|
|
634
|
+
}
|
|
574
635
|
|
|
575
636
|
.thread-surface {
|
|
576
637
|
display: flex;
|
|
@@ -578,7 +639,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
578
639
|
gap: 14px;
|
|
579
640
|
min-height: 0;
|
|
580
641
|
flex: 1;
|
|
581
|
-
padding:
|
|
642
|
+
padding: 22px 22px 18px;
|
|
582
643
|
border-radius: 28px;
|
|
583
644
|
border: 1px solid rgba(31, 67, 125, 0.1);
|
|
584
645
|
background:
|
|
@@ -622,11 +683,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
622
683
|
}
|
|
623
684
|
.message.manager {
|
|
624
685
|
justify-items: end;
|
|
625
|
-
padding-left:
|
|
686
|
+
padding-left: 30%;
|
|
626
687
|
}
|
|
627
688
|
.message.employee {
|
|
628
689
|
justify-items: start;
|
|
629
|
-
padding-right:
|
|
690
|
+
padding-right: 30%;
|
|
630
691
|
}
|
|
631
692
|
.message.system {
|
|
632
693
|
justify-items: stretch;
|
|
@@ -687,11 +748,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
687
748
|
}
|
|
688
749
|
.bubble {
|
|
689
750
|
border-radius: 26px;
|
|
690
|
-
padding: 16px
|
|
751
|
+
padding: 14px 16px;
|
|
691
752
|
font-size: 14px;
|
|
692
|
-
line-height: 1.
|
|
753
|
+
line-height: 1.55;
|
|
693
754
|
box-shadow: 0 12px 26px rgba(35, 30, 23, 0.05);
|
|
694
|
-
max-width: min(
|
|
755
|
+
max-width: min(620px, 100%);
|
|
695
756
|
}
|
|
696
757
|
.message.manager .bubble {
|
|
697
758
|
background: var(--accent);
|
|
@@ -750,8 +811,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
750
811
|
|
|
751
812
|
.coach textarea {
|
|
752
813
|
width: 100%;
|
|
753
|
-
min-height:
|
|
754
|
-
max-height:
|
|
814
|
+
min-height: 48px;
|
|
815
|
+
max-height: 22vh;
|
|
755
816
|
resize: vertical;
|
|
756
817
|
border: 1px solid var(--line);
|
|
757
818
|
border-radius: 18px;
|
|
@@ -762,17 +823,17 @@ button { font: inherit; cursor: pointer; }
|
|
|
762
823
|
}
|
|
763
824
|
.coach textarea:focus { outline: none; border-color: var(--accent); }
|
|
764
825
|
.coach {
|
|
765
|
-
background:
|
|
766
|
-
border:
|
|
767
|
-
border-radius:
|
|
768
|
-
padding:
|
|
826
|
+
background: transparent;
|
|
827
|
+
border: none;
|
|
828
|
+
border-radius: 0;
|
|
829
|
+
padding: 0;
|
|
769
830
|
}
|
|
770
|
-
.coach-actions { display: flex; justify-content: flex-end; margin-top:
|
|
831
|
+
.coach-actions { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
771
832
|
.coach-note {
|
|
772
|
-
margin-top:
|
|
833
|
+
margin-top: 8px;
|
|
773
834
|
color: var(--muted);
|
|
774
|
-
font-size:
|
|
775
|
-
line-height: 1.
|
|
835
|
+
font-size: 11px;
|
|
836
|
+
line-height: 1.4;
|
|
776
837
|
}
|
|
777
838
|
.send-button {
|
|
778
839
|
background: var(--accent);
|
|
@@ -787,14 +848,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
787
848
|
.send-button:disabled { background: #c5d2cb; cursor: not-allowed; }
|
|
788
849
|
|
|
789
850
|
.micro {
|
|
790
|
-
margin-top:
|
|
791
|
-
border-top: 1px solid var(--line);
|
|
792
|
-
padding-top: 14px;
|
|
851
|
+
margin-top: 0;
|
|
793
852
|
position: sticky;
|
|
794
853
|
bottom: 0;
|
|
795
|
-
z-index:
|
|
796
|
-
background:
|
|
797
|
-
linear-gradient(180deg, rgba(248, 244, 235, 0), rgba(248, 244, 235, 0.96) 22%);
|
|
854
|
+
z-index: 2;
|
|
855
|
+
background: rgba(255, 255, 255, 0.4);
|
|
798
856
|
}
|
|
799
857
|
.micro summary {
|
|
800
858
|
cursor: pointer;
|
|
@@ -812,10 +870,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
812
870
|
transition: transform 100ms;
|
|
813
871
|
}
|
|
814
872
|
.micro[open] summary::before { transform: rotate(90deg); }
|
|
815
|
-
.micro-log {
|
|
816
|
-
margin
|
|
817
|
-
background: #1f2a24;
|
|
818
|
-
color: #c5d2cb;
|
|
873
|
+
.micro-log {
|
|
874
|
+
margin: 0 16px 16px;
|
|
875
|
+
background: #1f2a24;
|
|
876
|
+
color: #c5d2cb;
|
|
819
877
|
border-radius: 8px;
|
|
820
878
|
padding: 12px 14px;
|
|
821
879
|
font-family: Consolas, Menlo, monospace;
|
|
@@ -1025,6 +1083,9 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
1025
1083
|
.thread-surface {
|
|
1026
1084
|
padding: 16px 14px 14px;
|
|
1027
1085
|
}
|
|
1086
|
+
.panel-summary-text {
|
|
1087
|
+
white-space: normal;
|
|
1088
|
+
}
|
|
1028
1089
|
.message,
|
|
1029
1090
|
.message.manager,
|
|
1030
1091
|
.message.employee,
|
|
@@ -1217,14 +1278,14 @@ button.small { padding: 4px 10px; font-size: 12px; }
|
|
|
1217
1278
|
|
|
1218
1279
|
/* Totals line (R4). 12px muted, no border, single row that wraps if it
|
|
1219
1280
|
absolutely must. Discoverable via hover tooltips per spec R4.4. */
|
|
1220
|
-
.totals {
|
|
1221
|
-
font-size:
|
|
1222
|
-
color: var(--muted);
|
|
1223
|
-
padding-top:
|
|
1224
|
-
display: flex;
|
|
1225
|
-
gap:
|
|
1226
|
-
flex-wrap: wrap;
|
|
1227
|
-
}
|
|
1281
|
+
.totals {
|
|
1282
|
+
font-size: 11px;
|
|
1283
|
+
color: var(--muted);
|
|
1284
|
+
padding-top: 2px;
|
|
1285
|
+
display: flex;
|
|
1286
|
+
gap: 10px;
|
|
1287
|
+
flex-wrap: wrap;
|
|
1288
|
+
}
|
|
1228
1289
|
.totals span { cursor: help; }
|
|
1229
1290
|
.totals .sep { color: var(--line); }
|
|
1230
1291
|
.totals strong { color: var(--text); font-weight: 600; }
|