fraim-hub 2.0.333 → 2.0.335
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/package.json +2 -2
- package/public/ai-hub/index.html +25 -49
- package/public/ai-hub/review.css +3 -2
- package/public/ai-hub/script.js +219 -227
- package/public/ai-hub/styles.css +46 -215
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.335",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"author": "Sid Mathur <sid.mathur@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/mathursrus/FRAIM#readme",
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"electron-updater": "^6.8.9",
|
|
222
222
|
"express": "^5.2.1",
|
|
223
223
|
"extract-zip": "^2.0.1",
|
|
224
|
-
"fraim": "2.0.
|
|
224
|
+
"fraim": "2.0.335",
|
|
225
225
|
"mongodb": "^7.0.0",
|
|
226
226
|
"node-cron": "4.2.1",
|
|
227
227
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -437,12 +437,12 @@
|
|
|
437
437
|
</div>
|
|
438
438
|
|
|
439
439
|
<div class="manage-panel-shell">
|
|
440
|
-
<
|
|
441
|
-
<
|
|
440
|
+
<section class="panel-details panel-details--thread" id="thread-panel">
|
|
441
|
+
<div class="thread-header">
|
|
442
442
|
<span class="panel-summary-copy">
|
|
443
443
|
<span class="panel-kicker" id="thread-panel-kicker">Manage</span>
|
|
444
444
|
</span>
|
|
445
|
-
</
|
|
445
|
+
</div>
|
|
446
446
|
<!-- Issue #1065: find a passage inside the open conversation (R44-R46). A result that
|
|
447
447
|
promises "+6 more matches" is an unkept promise without this: the manager lands in
|
|
448
448
|
a thread that can run to hundreds of messages with no way to reach the match. -->
|
|
@@ -473,26 +473,13 @@
|
|
|
473
473
|
</div>
|
|
474
474
|
</div>
|
|
475
475
|
</div>
|
|
476
|
-
</
|
|
476
|
+
</section>
|
|
477
477
|
<div class="manage-header-actions">
|
|
478
|
-
<button class="conversation-focus-btn" id="conversation-focus-btn" type="button" aria-label="Expand conversation" title="Expand conversation">↗</button>
|
|
479
478
|
<button class="micro-manage-toggle" id="micro-manage-toggle" type="button" role="switch" aria-checked="false" aria-controls="manage-body"><span>Micro-Manage</span><span class="micro-switch-track" aria-hidden="true"></span></button>
|
|
480
479
|
</div>
|
|
481
480
|
</div>
|
|
482
481
|
|
|
483
482
|
<div class="support-stack">
|
|
484
|
-
<!-- Issue #770: single-run deliverables live here (a stable collapsible
|
|
485
|
-
panel), not pinned to the bottom of the thread. Static-and-hidden,
|
|
486
|
-
populated + toggled by renderDeliverablesPanel. -->
|
|
487
|
-
<details class="panel-details panel-details--deliverables" id="deliverables-panel" hidden>
|
|
488
|
-
<summary>
|
|
489
|
-
<span class="panel-summary-copy">
|
|
490
|
-
<span class="panel-kicker">Deliverables</span>
|
|
491
|
-
<span class="panel-summary-text" id="deliverables-summary"></span>
|
|
492
|
-
</span>
|
|
493
|
-
</summary>
|
|
494
|
-
<div class="panel-body" id="deliverables-body"></div>
|
|
495
|
-
</details>
|
|
496
483
|
<!-- Issue #1292: when this conversation's own agent stops being available,
|
|
497
484
|
say so. The selector keeps holding that agent; it is never quietly
|
|
498
485
|
swapped for another one. Lives outside the composer controls so
|
|
@@ -519,23 +506,23 @@
|
|
|
519
506
|
</div>
|
|
520
507
|
</div>
|
|
521
508
|
|
|
522
|
-
<
|
|
523
|
-
<
|
|
509
|
+
<section class="panel-details panel-details--thread" id="ab-direct-thread-panel">
|
|
510
|
+
<div class="thread-header">
|
|
524
511
|
<span class="panel-summary-copy">
|
|
525
512
|
<span class="panel-kicker">Direct output (raw)</span>
|
|
526
513
|
</span>
|
|
527
|
-
</
|
|
514
|
+
</div>
|
|
528
515
|
<pre class="ab-direct-log" id="ab-direct-log"></pre>
|
|
529
|
-
</
|
|
516
|
+
</section>
|
|
530
517
|
|
|
531
518
|
<div class="support-stack">
|
|
532
|
-
<
|
|
533
|
-
<
|
|
519
|
+
<section class="panel-details panel-details--coach" id="ab-direct-reply-panel">
|
|
520
|
+
<div class="thread-header">
|
|
534
521
|
<span class="panel-summary-copy">
|
|
535
522
|
<span class="panel-kicker">Reply to Direct agent</span>
|
|
536
|
-
<span class="panel-summary-text" id="ab-direct-reply-summary">
|
|
523
|
+
<span class="panel-summary-text" id="ab-direct-reply-summary">Send a follow-up</span>
|
|
537
524
|
</span>
|
|
538
|
-
</
|
|
525
|
+
</div>
|
|
539
526
|
<div class="panel-body">
|
|
540
527
|
<div class="coach">
|
|
541
528
|
<div class="coach-input">
|
|
@@ -544,7 +531,7 @@
|
|
|
544
531
|
</div>
|
|
545
532
|
</div>
|
|
546
533
|
</div>
|
|
547
|
-
</
|
|
534
|
+
</section>
|
|
548
535
|
</div>
|
|
549
536
|
</div>
|
|
550
537
|
|
|
@@ -555,9 +542,7 @@
|
|
|
555
542
|
visible even when no conversation is active"). Only the content
|
|
556
543
|
ABOVE this div changes (idle placeholder, hire strip, thread, or
|
|
557
544
|
the A/B compare panel); the composer itself never opens, closes,
|
|
558
|
-
or relocates
|
|
559
|
-
"expand conversation", handled by openConversationFocus/
|
|
560
|
-
closeConversationFocus alongside #thread-panel. -->
|
|
545
|
+
or relocates. -->
|
|
561
546
|
<div id="persistent-composer">
|
|
562
547
|
<div class="composer-suggestions" id="composer-suggestions" hidden>
|
|
563
548
|
<div id="cp-results" class="cp-results-scroll">
|
|
@@ -589,14 +574,10 @@
|
|
|
589
574
|
<button type="button" id="cp-armed-job-clear" aria-label="Change job">×</button>
|
|
590
575
|
</div>
|
|
591
576
|
<div class="staged-attachments" id="staged-attachments"></div>
|
|
592
|
-
|
|
593
|
-
<button class="rb-approve" type="button" id="review-approve">✓ Approve</button>
|
|
594
|
-
<button class="rb-secondary" type="button" id="review-request-changes">↩ Request changes</button>
|
|
595
|
-
<span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
|
|
596
|
-
</div>
|
|
577
|
+
|
|
597
578
|
<div class="coach-input">
|
|
598
579
|
<input type="file" id="attach-file-input" multiple accept="image/png,image/jpeg,image/gif,.pdf,.txt,.log" hidden>
|
|
599
|
-
<textarea id="coach-text" aria-label="Type an instruction" placeholder="Type a job name, /handle, or describe what you need..." data-placeholder-idle="Type a job name, /handle, or describe what you need..." data-placeholder-active="Coach this run, or type new instructions..." data-placeholder-armed="Now type what you want done..."></textarea>
|
|
580
|
+
<textarea id="coach-text" aria-label="Type an instruction" placeholder="Type a job name, /handle, or describe what you need..." data-placeholder-idle="Type a job name, /handle, or describe what you need..." data-placeholder-review="Reply to continue the run, or use Approve / Request changes above..." data-placeholder-active="Coach this run, or type new instructions..." data-placeholder-armed="Now type what you want done..."></textarea>
|
|
600
581
|
<div class="composer-footer">
|
|
601
582
|
<button class="attach-btn" type="button" id="attach-btn" aria-label="Attach a file or screenshot" title="Attach a file or screenshot">📎</button>
|
|
602
583
|
<div class="quick-coach-row composer-coach-actions" id="quick-coach-btns" hidden>
|
|
@@ -683,7 +664,7 @@
|
|
|
683
664
|
</main>
|
|
684
665
|
</div>
|
|
685
666
|
|
|
686
|
-
|
|
667
|
+
|
|
687
668
|
</div>
|
|
688
669
|
|
|
689
670
|
<!-- Issue #1065: search results replace the conversation panel while a query is active,
|
|
@@ -1130,19 +1111,6 @@
|
|
|
1130
1111
|
</div>
|
|
1131
1112
|
</div>
|
|
1132
1113
|
|
|
1133
|
-
<!-- Issue #539: Command palette overlay — replaces 2-step modal as job-launch surface -->
|
|
1134
|
-
<section class="conversation-focus-overlay" id="conversation-focus-overlay" role="dialog" aria-modal="true" aria-labelledby="conversation-focus-title" hidden>
|
|
1135
|
-
<div class="conversation-focus-dialog">
|
|
1136
|
-
<div class="conversation-focus-header">
|
|
1137
|
-
<div class="conversation-focus-copy">
|
|
1138
|
-
<strong id="conversation-focus-title">Focused conversation</strong>
|
|
1139
|
-
<span id="conversation-focus-subtitle">Conversation thread and coaching</span>
|
|
1140
|
-
</div>
|
|
1141
|
-
<button class="conversation-focus-close" id="conversation-focus-close" type="button" aria-label="Close focused conversation">Close</button>
|
|
1142
|
-
</div>
|
|
1143
|
-
<div class="conversation-focus-panels" id="conversation-focus-panels"></div>
|
|
1144
|
-
</div>
|
|
1145
|
-
</section>
|
|
1146
1114
|
|
|
1147
1115
|
<!-- Issue #1657 Treatment 3: #cp-modal is retired as a separate dialog.
|
|
1148
1116
|
The ids below (cp-recent-list, cp-catalog-list, cp-agent-picker, etc.)
|
|
@@ -1265,7 +1233,15 @@
|
|
|
1265
1233
|
</div>
|
|
1266
1234
|
</div>
|
|
1267
1235
|
|
|
1236
|
+
<div id="review-action-storage" hidden>
|
|
1237
|
+
<div class="review-actions" id="review-actions" hidden>
|
|
1238
|
+
<button class="rb-approve" type="button" id="review-approve">✓ Approve</button>
|
|
1239
|
+
<button class="rb-secondary" type="button" id="review-request-changes">↩ Request changes</button>
|
|
1240
|
+
<span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
|
|
1241
|
+
</div>
|
|
1242
|
+
</div>
|
|
1268
1243
|
<script src="./script.js?v=dark-status-20260721"></script>
|
|
1269
1244
|
<script src="./remote-ui-loader.js?v=server-driven-ui-20260724"></script>
|
|
1245
|
+
|
|
1270
1246
|
</body>
|
|
1271
1247
|
</html>
|
package/public/ai-hub/review.css
CHANGED
|
@@ -422,8 +422,6 @@
|
|
|
422
422
|
.rev-item .rev-dismiss:hover { background: var(--soft); }
|
|
423
423
|
|
|
424
424
|
/* ── #770: Deliverables panel body spacing for the relocated artifact strip. ── */
|
|
425
|
-
#deliverables-panel .panel-body { padding-top: 8px; }
|
|
426
|
-
#deliverables-panel .rc-artifact-strip { margin: 0; }
|
|
427
425
|
|
|
428
426
|
/* ── R9: project-scoped learnings panel (over the workspace conversation) ── */
|
|
429
427
|
.proj-learnings-panel {
|
|
@@ -633,3 +631,6 @@
|
|
|
633
631
|
flex-direction: column;
|
|
634
632
|
gap: 6px;
|
|
635
633
|
}
|
|
634
|
+
|
|
635
|
+
.review-turn .review-actions { border: 0; margin: 0; padding: 0; }
|
|
636
|
+
.review-turn .rb-handoff { flex-basis: 100%; }
|
package/public/ai-hub/script.js
CHANGED
|
@@ -140,10 +140,9 @@ function gatherElements() {
|
|
|
140
140
|
// Issue #385: team roster
|
|
141
141
|
'team-roster',
|
|
142
142
|
'empty', 'active-conv', 'active-title', 'active-identity', 'run-state-pill', 'run-stop-btn',
|
|
143
|
-
'conversation-focus-btn', 'conversation-focus-overlay', 'conversation-focus-close', 'conversation-focus-title', 'conversation-focus-subtitle', 'conversation-focus-panels',
|
|
144
143
|
'messages',
|
|
145
144
|
'coach-text', 'send', 'micro-manage', 'micro-log', 'resume-command', 'resume-command-code', 'resume-command-copy',
|
|
146
|
-
'
|
|
145
|
+
'coach-note',
|
|
147
146
|
// Issue #1651: composer attachments.
|
|
148
147
|
'coach', 'composer-dropzone', 'staged-attachments', 'attach-btn', 'attach-file-input',
|
|
149
148
|
'thread-panel', 'thread-panel-kicker', 'micro-manage-toggle', 'micro-return', 'micro-activity-state',
|
|
@@ -171,8 +170,6 @@ function gatherElements() {
|
|
|
171
170
|
'word-context-bar', 'word-ctx-text', 'word-ctx-refresh',
|
|
172
171
|
// Issue #512 S6: review experience (R7) + learnings (R9) + reverse mentoring (R10).
|
|
173
172
|
'review-actions', 'review-approve', 'review-request-changes', 'review-handoff',
|
|
174
|
-
// Issue #770: Deliverables panel (single-run artifact home).
|
|
175
|
-
'deliverables-panel', 'deliverables-summary', 'deliverables-body',
|
|
176
173
|
// Issue #540: persona grid, manager team pool, hire strip, hire checkout modal.
|
|
177
174
|
'persona-grid', 'manager-team-pool',
|
|
178
175
|
'hire-strip', 'hs-avatar', 'hs-title', 'hs-sub', 'hs-add-btn', 'hs-get-btn', 'hs-skip-btn',
|
|
@@ -366,8 +363,7 @@ function applyBootstrap(bootstrap, docUrl) {
|
|
|
366
363
|
}
|
|
367
364
|
// Render header project name + status line.
|
|
368
365
|
els['project-name'].textContent = friendlyProjectName(bootstrap.project.path);
|
|
369
|
-
|
|
370
|
-
els['status-line'].classList.toggle('error', !bootstrap.project.exists || !bootstrap.project.hasFraim);
|
|
366
|
+
if (!bootstrap.project.exists || !bootstrap.project.hasFraim) showStatus(bootstrap.project.message, true);
|
|
371
367
|
// Update the compact project label shown in task-pane surface rail.
|
|
372
368
|
const tpLabel = document.getElementById('tp-project-label');
|
|
373
369
|
if (tpLabel) {
|
|
@@ -2181,6 +2177,8 @@ function syncComposerPlaceholder(conv = activeConversation()) {
|
|
|
2181
2177
|
if (!textarea || textarea.disabled) return;
|
|
2182
2178
|
if (state.cpSelectedJob) {
|
|
2183
2179
|
textarea.placeholder = textarea.dataset.placeholderArmed || 'Now type what you want done...';
|
|
2180
|
+
} else if (convAwaitingReview(conv)) {
|
|
2181
|
+
textarea.placeholder = textarea.dataset.placeholderReview;
|
|
2184
2182
|
} else if (conv) {
|
|
2185
2183
|
textarea.placeholder = textarea.dataset.placeholderActive || 'Coach this run, or type new instructions...';
|
|
2186
2184
|
} else {
|
|
@@ -2323,7 +2321,7 @@ function manageViewFor(conv) {
|
|
|
2323
2321
|
}
|
|
2324
2322
|
function captureManagePosition() {
|
|
2325
2323
|
const view = manageViews.get(displayedManageKey);
|
|
2326
|
-
if (!view
|
|
2324
|
+
if (!view) return;
|
|
2327
2325
|
const node = view.micro ? els['micro-log'] : els['messages'];
|
|
2328
2326
|
if (!node || !node.clientHeight) return;
|
|
2329
2327
|
view[view.micro ? 'activity' : 'messages'] = {
|
|
@@ -2351,7 +2349,6 @@ function setManageMicroMode(enabled) {
|
|
|
2351
2349
|
if (!conv || conv.compareMode === 'ab') return;
|
|
2352
2350
|
captureManagePosition();
|
|
2353
2351
|
manageViewFor(conv).micro = enabled;
|
|
2354
|
-
els['thread-panel'].open = true;
|
|
2355
2352
|
applyManageView(conv);
|
|
2356
2353
|
syncMicroArchive(conv);
|
|
2357
2354
|
restoreManagePosition(conv);
|
|
@@ -2372,15 +2369,6 @@ function syncManageActivityState(conv, events) {
|
|
|
2372
2369
|
els['micro-return'].hidden = !(error || conversationUiState(conv) === 'waiting' || convAwaitingReview(conv));
|
|
2373
2370
|
}
|
|
2374
2371
|
|
|
2375
|
-
function syncConversationPanels(conv, switchedConv) {
|
|
2376
|
-
if (!conv) return;
|
|
2377
|
-
const panelState = panelStateFor(conv.id);
|
|
2378
|
-
if (switchedConv) {
|
|
2379
|
-
if (els['thread-panel']) {
|
|
2380
|
-
els['thread-panel'].open = panelState.thread ?? true;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
2372
|
|
|
2385
2373
|
function upsertConversation(conv, options) {
|
|
2386
2374
|
// Issue #708: route to the conversation's scope bucket (manager/company → sentinel
|
|
@@ -2402,6 +2390,24 @@ function newConversationId() {
|
|
|
2402
2390
|
// #533 R6: remove a run from the Hub. This is a Hub-local projection delete — it
|
|
2403
2391
|
// drops the conversation from state + localStorage and clears any pointer to it.
|
|
2404
2392
|
// It never touches artifacts the employee wrote to disk (the confirm copy says so).
|
|
2393
|
+
function clearDeletedConversationReferences(id) {
|
|
2394
|
+
if (!id) return;
|
|
2395
|
+
if (activeConversationId === id || state.activeId === id || deletedConversationIds.has(activeConversationId)) {
|
|
2396
|
+
activeConversationId = null;
|
|
2397
|
+
state.activeId = null;
|
|
2398
|
+
}
|
|
2399
|
+
if (state.areaActiveId) {
|
|
2400
|
+
for (const area of Object.keys(state.areaActiveId)) {
|
|
2401
|
+
if (state.areaActiveId[area] === id) state.areaActiveId[area] = null;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (typeof tf === 'object' && tf && tf.areaSelection) {
|
|
2405
|
+
for (const area of Object.keys(tf.areaSelection)) {
|
|
2406
|
+
if (tf.areaSelection[area] === id) tf.areaSelection[area] = null;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2405
2411
|
async function deleteConversation(id) {
|
|
2406
2412
|
if (!id) return false;
|
|
2407
2413
|
const target = findConversation(id);
|
|
@@ -2410,9 +2416,8 @@ async function deleteConversation(id) {
|
|
|
2410
2416
|
// Tombstone before the async delete so any in-flight header/body hydrate that
|
|
2411
2417
|
// resolves during the server round trip cannot reselect or reinsert this run.
|
|
2412
2418
|
deletedConversationIds.add(id);
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
state.activeId = null;
|
|
2419
|
+
clearDeletedConversationReferences(id);
|
|
2420
|
+
if (!state.activeId) {
|
|
2416
2421
|
if (state.pollHandle) { window.clearInterval(state.pollHandle); state.pollHandle = null; }
|
|
2417
2422
|
}
|
|
2418
2423
|
try {
|
|
@@ -2431,12 +2436,10 @@ async function deleteConversation(id) {
|
|
|
2431
2436
|
if (next.length !== list.length) { state.conversations[key] = next; removed = true; }
|
|
2432
2437
|
}
|
|
2433
2438
|
const clearDeletedActive = () => {
|
|
2434
|
-
|
|
2435
|
-
activeConversationId = null;
|
|
2436
|
-
state.activeId = null;
|
|
2439
|
+
clearDeletedConversationReferences(id);
|
|
2437
2440
|
// Deleting the active run: stop its poll so no zombie interval keeps ticking
|
|
2438
2441
|
// against a conversation that no longer exists.
|
|
2439
|
-
if (state.pollHandle) { window.clearInterval(state.pollHandle); state.pollHandle = null; }
|
|
2442
|
+
if (!state.activeId && state.pollHandle) { window.clearInterval(state.pollHandle); state.pollHandle = null; }
|
|
2440
2443
|
};
|
|
2441
2444
|
clearDeletedActive();
|
|
2442
2445
|
if (!removed) return false;
|
|
@@ -3856,18 +3859,6 @@ function syncMicroArchive(conv) {
|
|
|
3856
3859
|
els['micro-log'].onscroll = captureManagePosition;
|
|
3857
3860
|
microArchiveTimer = setInterval(() => { if (microArchiveIsVisible(conv)) void loadMicroArchive(conv); }, 2000);
|
|
3858
3861
|
}
|
|
3859
|
-
const conversationFocusState = {
|
|
3860
|
-
open: false,
|
|
3861
|
-
previousFocus: null,
|
|
3862
|
-
threadParent: null,
|
|
3863
|
-
threadNext: null,
|
|
3864
|
-
// Issue #1657 Treatment 3: the composer is permanently docked; "expand
|
|
3865
|
-
// conversation" moves it into the overlay alongside the thread so the
|
|
3866
|
-
// manager can still type while expanded.
|
|
3867
|
-
composerParent: null,
|
|
3868
|
-
composerNext: null,
|
|
3869
|
-
messageScrollTop: 0,
|
|
3870
|
-
};
|
|
3871
3862
|
// #521 UI rework: the project workspace shows EITHER a job conversation (full
|
|
3872
3863
|
// height) OR the Brief & Learnings page — never both stacked, so the thread,
|
|
3873
3864
|
// coach, and micro-manage all have room. Toggle the mode class the CSS keys off,
|
|
@@ -3908,110 +3899,12 @@ function formatMicroLogLine(event, mode = 'events') {
|
|
|
3908
3899
|
return `[${channel}] ${event && event.text != null ? String(event.text) : ''}\n`;
|
|
3909
3900
|
}
|
|
3910
3901
|
|
|
3911
|
-
function syncConversationFocusTitle(conv) {
|
|
3912
|
-
if (!els['conversation-focus-title']) return;
|
|
3913
|
-
els['conversation-focus-title'].textContent = conv ? conversationTitle(conv) : 'Focused conversation';
|
|
3914
|
-
if (els['conversation-focus-subtitle']) {
|
|
3915
|
-
els['conversation-focus-subtitle'].textContent = 'Conversation thread and coaching';
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
|
|
3919
|
-
function restoreFocusedConversationNodes() {
|
|
3920
|
-
const thread = els['thread-panel'].closest('.manage-panel-shell') || els['thread-panel'];
|
|
3921
|
-
const composer = document.getElementById('persistent-composer');
|
|
3922
|
-
if (thread && conversationFocusState.threadParent) {
|
|
3923
|
-
const next = conversationFocusState.threadNext;
|
|
3924
|
-
if (next && next.parentNode === conversationFocusState.threadParent) {
|
|
3925
|
-
conversationFocusState.threadParent.insertBefore(thread, next);
|
|
3926
|
-
} else {
|
|
3927
|
-
conversationFocusState.threadParent.appendChild(thread);
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3930
|
-
if (composer && conversationFocusState.composerParent) {
|
|
3931
|
-
const next = conversationFocusState.composerNext;
|
|
3932
|
-
if (next && next.parentNode === conversationFocusState.composerParent) {
|
|
3933
|
-
conversationFocusState.composerParent.insertBefore(composer, next);
|
|
3934
|
-
} else {
|
|
3935
|
-
conversationFocusState.composerParent.appendChild(composer);
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
function closeConversationFocus() {
|
|
3941
|
-
if (!conversationFocusState.open) return;
|
|
3942
|
-
captureManagePosition();
|
|
3943
|
-
if (els['messages']) conversationFocusState.messageScrollTop = els['messages'].scrollTop;
|
|
3944
|
-
restoreFocusedConversationNodes();
|
|
3945
|
-
conversationFocusState.open = false;
|
|
3946
|
-
document.body.classList.remove('conversation-focus-open');
|
|
3947
|
-
if (els['conversation-focus-overlay']) {
|
|
3948
|
-
els['conversation-focus-overlay'].hidden = true;
|
|
3949
|
-
els['conversation-focus-overlay'].classList.remove('open');
|
|
3950
|
-
}
|
|
3951
|
-
requestAnimationFrame(() => {
|
|
3952
|
-
if (els['messages']) els['messages'].scrollTop = conversationFocusState.messageScrollTop || 0;
|
|
3953
|
-
if (typeof syncThreadMessageViewport === 'function') syncThreadMessageViewport();
|
|
3954
|
-
const conv = activeConversation();
|
|
3955
|
-
if (conv) restoreManagePosition(conv);
|
|
3956
|
-
const target = conversationFocusState.previousFocus && document.contains(conversationFocusState.previousFocus)
|
|
3957
|
-
? conversationFocusState.previousFocus
|
|
3958
|
-
: els['conversation-focus-btn'];
|
|
3959
|
-
if (target && typeof target.focus === 'function') target.focus();
|
|
3960
|
-
});
|
|
3961
|
-
}
|
|
3962
|
-
|
|
3963
|
-
function openConversationFocus() {
|
|
3964
|
-
const conv = activeConversation();
|
|
3965
|
-
captureManagePosition();
|
|
3966
|
-
const overlay = els['conversation-focus-overlay'];
|
|
3967
|
-
const panels = els['conversation-focus-panels'];
|
|
3968
|
-
const thread = els['thread-panel'].closest('.manage-panel-shell') || els['thread-panel'];
|
|
3969
|
-
const composer = document.getElementById('persistent-composer');
|
|
3970
|
-
if (!conv || !overlay || !panels || !thread) return;
|
|
3971
|
-
if (conversationFocusState.open) return;
|
|
3972
|
-
|
|
3973
|
-
conversationFocusState.open = true;
|
|
3974
|
-
conversationFocusState.previousFocus = document.activeElement;
|
|
3975
|
-
conversationFocusState.threadParent = thread.parentNode;
|
|
3976
|
-
conversationFocusState.threadNext = thread.nextSibling;
|
|
3977
|
-
if (composer) {
|
|
3978
|
-
conversationFocusState.composerParent = composer.parentNode;
|
|
3979
|
-
conversationFocusState.composerNext = composer.nextSibling;
|
|
3980
|
-
}
|
|
3981
|
-
conversationFocusState.messageScrollTop = els['messages'] ? els['messages'].scrollTop : 0;
|
|
3982
|
-
|
|
3983
|
-
syncConversationFocusTitle(conv);
|
|
3984
|
-
panels.appendChild(thread);
|
|
3985
|
-
if (composer) panels.appendChild(composer);
|
|
3986
|
-
overlay.hidden = false;
|
|
3987
|
-
overlay.classList.add('open');
|
|
3988
|
-
document.body.classList.add('conversation-focus-open');
|
|
3989
|
-
|
|
3990
|
-
requestAnimationFrame(() => {
|
|
3991
|
-
if (els['messages']) els['messages'].scrollTop = conversationFocusState.messageScrollTop || 0;
|
|
3992
|
-
if (typeof syncThreadMessageViewport === 'function') syncThreadMessageViewport();
|
|
3993
|
-
restoreManagePosition(conv);
|
|
3994
|
-
if (els['conversation-focus-close']) els['conversation-focus-close'].focus();
|
|
3995
|
-
});
|
|
3996
|
-
}
|
|
3997
|
-
|
|
3998
|
-
function handleConversationFocusEscape(e) {
|
|
3999
|
-
if (e.key !== 'Escape' || !conversationFocusState.open || e.defaultPrevented) return;
|
|
4000
|
-
const openCoachMenu = COACH_DROPDOWN_PAIRS
|
|
4001
|
-
.some(([id]) => { const menu = els[id] || document.getElementById(id); return menu && !menu.hidden; });
|
|
4002
|
-
const reviewMenu = document.getElementById('review-approve-menu');
|
|
4003
|
-
if (openCoachMenu || (reviewMenu && !reviewMenu.hidden)) return;
|
|
4004
|
-
e.preventDefault();
|
|
4005
|
-
closeConversationFocus();
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
3902
|
function renderActive() {
|
|
4009
3903
|
const conv = activeConversation();
|
|
4010
3904
|
if (typeof syncTaskPaneSurfaceMode === 'function') syncTaskPaneSurfaceMode();
|
|
4011
3905
|
if (typeof tfApplyWorkspaceMode === 'function') tfApplyWorkspaceMode();
|
|
4012
3906
|
if (!conv) {
|
|
4013
3907
|
syncMicroArchive(null);
|
|
4014
|
-
closeConversationFocus();
|
|
4015
3908
|
els['empty'].hidden = false;
|
|
4016
3909
|
els['active-conv'].hidden = true;
|
|
4017
3910
|
renderHubAgentSetupPanel();
|
|
@@ -4024,7 +3917,6 @@ function renderActive() {
|
|
|
4024
3917
|
syncComposerPlaceholder(null);
|
|
4025
3918
|
syncComposerStopButton(null);
|
|
4026
3919
|
// #770: hide the Deliverables panel when there is no active conversation.
|
|
4027
|
-
if (els['deliverables-panel']) { els['deliverables-panel'].hidden = true; els['deliverables-body'].innerHTML = ''; }
|
|
4028
3920
|
syncThreadUiState(null);
|
|
4029
3921
|
renderedConvId = null;
|
|
4030
3922
|
renderedMessageCount = 0;
|
|
@@ -4050,7 +3942,6 @@ function renderActive() {
|
|
|
4050
3942
|
syncConvictionDropdownLabel();
|
|
4051
3943
|
syncCoachNote(conv);
|
|
4052
3944
|
syncComposerPlaceholder(conv);
|
|
4053
|
-
if (conversationFocusState.open) syncConversationFocusTitle(conv);
|
|
4054
3945
|
|
|
4055
3946
|
// If we switched conversations (or this is the first render), wipe and
|
|
4056
3947
|
// start fresh. Otherwise we're going to do an incremental update below.
|
|
@@ -4094,7 +3985,6 @@ function renderActive() {
|
|
|
4094
3985
|
}
|
|
4095
3986
|
const statusChanged = renderedStatus !== conv.status;
|
|
4096
3987
|
syncThreadUiState(conv);
|
|
4097
|
-
syncConversationPanels(conv, switchedConv);
|
|
4098
3988
|
syncQuickCoachButtons(conv);
|
|
4099
3989
|
// Issue #1723: renderActive() runs on every poll cycle for the active
|
|
4100
3990
|
// conversation but previously never re-synced the Stop button here, so a
|
|
@@ -4245,13 +4135,14 @@ function renderActive() {
|
|
|
4245
4135
|
renderReviewExperience(conv);
|
|
4246
4136
|
// Issue #848 — "What's next?" chips below a completed conversation (post-retro).
|
|
4247
4137
|
renderNextJobRecommendations(conv);
|
|
4138
|
+
renderDeliverablesBar(conv);
|
|
4248
4139
|
syncManagedDelegationAccess(conv);
|
|
4249
4140
|
syncThreadMessageViewport();
|
|
4250
4141
|
// Issue #1065: the thread is rebuilt from state on a poll cadence, which would wipe the find
|
|
4251
4142
|
// marks a manager is stepping through. Re-emit them from find state instead of injecting once.
|
|
4252
4143
|
if (typeof convFindRefresh === 'function') convFindRefresh();
|
|
4253
4144
|
const shouldScrollForUpdate = switchedConv || statusChanged || appendedMessages > 0 || messagesMutated;
|
|
4254
|
-
const reviewFollow = (switchedConv ||
|
|
4145
|
+
const reviewFollow = (switchedConv || threadWasNearBottom) && (!switchedConv || manageViewFor(conv).messages.follow);
|
|
4255
4146
|
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
|
|
4256
4147
|
requestAnimationFrame(() => {
|
|
4257
4148
|
syncThreadMessageViewport();
|
|
@@ -4273,7 +4164,7 @@ function renderActive() {
|
|
|
4273
4164
|
renderTracker(conv);
|
|
4274
4165
|
renderTotals(conv);
|
|
4275
4166
|
renderManagerOversightBoard(conv);
|
|
4276
|
-
|
|
4167
|
+
|
|
4277
4168
|
renderDelegationLedger(conv);
|
|
4278
4169
|
syncConversationRefreshPolling();
|
|
4279
4170
|
syncTemplatePickerVisibility();
|
|
@@ -4422,6 +4313,25 @@ function renderRunStatePill(conv) {
|
|
|
4422
4313
|
// conversationStateLabel returns 'Stopped' for that state; toUpperCase() => 'STOPPED'.
|
|
4423
4314
|
pill.textContent = conversationStateLabel(conv).toUpperCase();
|
|
4424
4315
|
pill.className = `run-state-pill ${conversationUiState(conv)}`;
|
|
4316
|
+
const pending = convAwaitingReview(conv);
|
|
4317
|
+
pill.tabIndex = pending ? 0 : -1;
|
|
4318
|
+
if (pending) {
|
|
4319
|
+
pill.setAttribute('role', 'button');
|
|
4320
|
+
pill.setAttribute('aria-label', 'Waiting on you. Go to the pending decision.');
|
|
4321
|
+
} else {
|
|
4322
|
+
pill.removeAttribute('role');
|
|
4323
|
+
pill.removeAttribute('aria-label');
|
|
4324
|
+
}
|
|
4325
|
+
const jump = () => {
|
|
4326
|
+
const current = activeConversation();
|
|
4327
|
+
if (!convAwaitingReview(current)) return;
|
|
4328
|
+
setManageMicroMode(false);
|
|
4329
|
+
els['thread-panel'].scrollIntoView({ block: 'nearest' });
|
|
4330
|
+
scrollThreadForReview(current);
|
|
4331
|
+
els['review-approve'].focus({ preventScroll: true });
|
|
4332
|
+
};
|
|
4333
|
+
pill.onclick = pending ? jump : null;
|
|
4334
|
+
pill.onkeydown = pending ? (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); jump(); } } : null;
|
|
4425
4335
|
syncComposerStopButton(conv);
|
|
4426
4336
|
}
|
|
4427
4337
|
|
|
@@ -5811,7 +5721,7 @@ function ensureThreadMessageViewportObserver() {
|
|
|
5811
5721
|
function syncThreadMessageViewport() {
|
|
5812
5722
|
const messages = els['messages'];
|
|
5813
5723
|
if (!messages) return;
|
|
5814
|
-
// Flex layout now owns the thread height: #thread-panel
|
|
5724
|
+
// Flex layout now owns the thread height: #thread-panel grows to fill the
|
|
5815
5725
|
// available real estate and #messages (flex child, overflow-y:auto) scrolls
|
|
5816
5726
|
// internally. Clear any legacy inline sizing so CSS stays authoritative.
|
|
5817
5727
|
messages.style.height = '';
|
|
@@ -5899,8 +5809,8 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom,
|
|
|
5899
5809
|
if (!latest || latest.id !== conv.id) return;
|
|
5900
5810
|
const host = els['messages'];
|
|
5901
5811
|
if (!host || host.hidden) return;
|
|
5812
|
+
ensurePendingAnchorScrollListener(host);
|
|
5902
5813
|
if (latest.status === 'running') {
|
|
5903
|
-
ensurePendingAnchorScrollListener(host);
|
|
5904
5814
|
if (!forceBottom && pendingAnchorUserScrolled(host)) {
|
|
5905
5815
|
return;
|
|
5906
5816
|
}
|
|
@@ -5970,7 +5880,7 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom,
|
|
|
5970
5880
|
}
|
|
5971
5881
|
}
|
|
5972
5882
|
return;
|
|
5973
|
-
} else if (shouldScrollForUpdate && reviewFollow) {
|
|
5883
|
+
} else if (shouldScrollForUpdate && reviewFollow && !pendingAnchorUserScrolled(host)) {
|
|
5974
5884
|
// Issue #1665: jump to the review target only on a real transition (switch or
|
|
5975
5885
|
// status change) or when the manager was already following the latest message.
|
|
5976
5886
|
// A live update landing while they read history must not yank the view.
|
|
@@ -5997,13 +5907,16 @@ function scrollThreadForReview(conv) {
|
|
|
5997
5907
|
const targetRect = target.getBoundingClientRect();
|
|
5998
5908
|
const currentTop = host.scrollTop;
|
|
5999
5909
|
const targetTopInsideHost = currentTop + (targetRect.top - hostRect.top);
|
|
6000
|
-
const
|
|
5910
|
+
const bar = host.querySelector('#deliverables-bar:not([hidden])');
|
|
5911
|
+
const reservedHeight = bar ? bar.getBoundingClientRect().height : 0;
|
|
5912
|
+
const availableLead = Math.max(0, host.clientHeight - reservedHeight - targetRect.height - 24);
|
|
5913
|
+
const reviewOffset = Math.min(Math.max(24, host.clientHeight * 0.16), availableLead);
|
|
6001
5914
|
const desiredTop = Math.max(0, targetTopInsideHost - reviewOffset);
|
|
6002
5915
|
// #936: use instant scroll so the animation completes synchronously. A smooth
|
|
6003
5916
|
// scroll takes ~300-500ms; if the user sets scrollTop before the animation
|
|
6004
5917
|
// finishes the CSS scroll-behavior causes the browser to re-animate, making
|
|
6005
5918
|
// the user's intent hard to honor. Instant + one-time gate = clean hand-off.
|
|
6006
|
-
host.
|
|
5919
|
+
host.scrollTo({ top: desiredTop, behavior: 'instant' });
|
|
6007
5920
|
}
|
|
6008
5921
|
|
|
6009
5922
|
// ---------------------------------------------------------------------------
|
|
@@ -6268,6 +6181,7 @@ function resizeComposerTextarea() {
|
|
|
6268
6181
|
}
|
|
6269
6182
|
|
|
6270
6183
|
function syncSendButton() {
|
|
6184
|
+
syncReviewHint();
|
|
6271
6185
|
resizeComposerTextarea();
|
|
6272
6186
|
const conv = activeConversation();
|
|
6273
6187
|
const hasText = els['coach-text'].value.trim().length > 0;
|
|
@@ -6981,37 +6895,95 @@ function deriveDeliverableFormat(conv) {
|
|
|
6981
6895
|
};
|
|
6982
6896
|
}
|
|
6983
6897
|
|
|
6898
|
+
// Review history belongs to the conversation record. It never grants approval:
|
|
6899
|
+
// the live run state remains the only source of action eligibility.
|
|
6900
|
+
function syncReviewTurns(conv) {
|
|
6901
|
+
if (!conv) return [];
|
|
6902
|
+
const turns = Array.isArray(conv.reviewTurns) ? conv.reviewTurns : [];
|
|
6903
|
+
if (!conversationHasBody(conv)) return turns;
|
|
6904
|
+
const awaiting = convAwaitingReview(conv);
|
|
6905
|
+
const signature = JSON.stringify(reviewHandoffForConversation(conv));
|
|
6906
|
+
let pending = turns.find((turn) => !turn.resolution);
|
|
6907
|
+
let changed = false;
|
|
6908
|
+
if (pending && (!awaiting || pending.signature !== signature)) {
|
|
6909
|
+
pending.resolution = conv.pauseReason === 'done' ? 'You marked this complete.'
|
|
6910
|
+
: conv.reviewApproved ? 'You approved this.'
|
|
6911
|
+
: awaiting ? 'A newer review replaced this decision.' : 'The run continued without a decision.';
|
|
6912
|
+
pending.resolvedAt = Date.now();
|
|
6913
|
+
pending = null;
|
|
6914
|
+
changed = true;
|
|
6915
|
+
}
|
|
6916
|
+
if (awaiting && !pending) {
|
|
6917
|
+
const last = (conv.messages || []).at(-1);
|
|
6918
|
+
turns.push({ id: String(Date.now()) + '-' + turns.length, signature,
|
|
6919
|
+
messageCount: (conv.messages || []).length, at: last ? messageTimestamp(last) : Date.now() });
|
|
6920
|
+
changed = true;
|
|
6921
|
+
}
|
|
6922
|
+
if (changed) {
|
|
6923
|
+
conv.reviewTurns = turns;
|
|
6924
|
+
persistConversation(conv);
|
|
6925
|
+
}
|
|
6926
|
+
return turns;
|
|
6927
|
+
}
|
|
6928
|
+
|
|
6929
|
+
function resolveReviewTurn(conv, resolution) {
|
|
6930
|
+
const turn = (conv.reviewTurns || syncReviewTurns(conv)).find((entry) => !entry.resolution);
|
|
6931
|
+
if (!turn) return;
|
|
6932
|
+
turn.resolution = resolution;
|
|
6933
|
+
turn.resolvedAt = Date.now();
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
function syncReviewHint(conv = activeConversation()) {
|
|
6937
|
+
const hint = els['review-handoff'];
|
|
6938
|
+
if (!hint || !convAwaitingReview(conv)) return;
|
|
6939
|
+
hint.textContent = els['coach-text'].value.trim()
|
|
6940
|
+
? 'Request changes will send what you typed. Send replies without deciding.'
|
|
6941
|
+
: 'Review the update and deliverables, then approve or type changes below. Send replies without deciding.';
|
|
6942
|
+
}
|
|
6943
|
+
|
|
6984
6944
|
function renderReviewExperience(conv) {
|
|
6985
6945
|
const actions = els['review-actions'];
|
|
6986
|
-
|
|
6987
|
-
|
|
6946
|
+
const host = els['messages'];
|
|
6947
|
+
if (!actions || !host) return;
|
|
6948
|
+
const turns = syncReviewTurns(conv);
|
|
6988
6949
|
const awaiting = convAwaitingReview(conv);
|
|
6950
|
+
const rows = Array.from(host.children).filter((node) => node.classList.contains('message') && !node.classList.contains('review-turn'));
|
|
6951
|
+
for (const turn of turns) {
|
|
6952
|
+
let row = Array.from(host.querySelectorAll('.review-turn')).find((node) => node.dataset.reviewTurn === turn.id);
|
|
6953
|
+
if (!row) {
|
|
6954
|
+
row = document.createElement('div');
|
|
6955
|
+
row.className = 'message employee review-turn';
|
|
6956
|
+
row.dataset.reviewTurn = turn.id;
|
|
6957
|
+
const bubble = document.createElement('div');
|
|
6958
|
+
bubble.className = 'bubble';
|
|
6959
|
+
const label = document.createElement('div');
|
|
6960
|
+
label.className = 'review-turn-label';
|
|
6961
|
+
label.textContent = 'Ready for your review';
|
|
6962
|
+
bubble.appendChild(label);
|
|
6963
|
+
row.appendChild(bubble);
|
|
6964
|
+
host.insertBefore(row, rows[turn.messageCount] || host.querySelector('#deliverables-bar, #next-job-section'));
|
|
6965
|
+
}
|
|
6966
|
+
const bubble = row.querySelector('.bubble');
|
|
6967
|
+
if (!turn.resolution && awaiting) {
|
|
6968
|
+
if (actions.parentNode !== bubble) bubble.appendChild(actions);
|
|
6969
|
+
actions.hidden = false;
|
|
6970
|
+
renderDynamicReviewActions(conv, reviewHandoffForConversation(conv));
|
|
6971
|
+
syncReviewHint(conv);
|
|
6972
|
+
} else {
|
|
6973
|
+
let settled = bubble.querySelector('.review-resolution');
|
|
6974
|
+
if (!settled) {
|
|
6975
|
+
settled = document.createElement('div');
|
|
6976
|
+
settled.className = 'review-resolution';
|
|
6977
|
+
bubble.appendChild(settled);
|
|
6978
|
+
}
|
|
6979
|
+
const at = new Date(turn.resolvedAt);
|
|
6980
|
+
settled.textContent = turn.resolution + (Number.isFinite(at.getTime()) ? ' ' + at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '');
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6989
6983
|
if (!awaiting) {
|
|
6990
6984
|
clearDynamicReviewActionButtons(actions);
|
|
6991
6985
|
actions.hidden = true;
|
|
6992
|
-
|
|
6993
|
-
}
|
|
6994
|
-
|
|
6995
|
-
const fmt = deriveDeliverableFormat(conv);
|
|
6996
|
-
|
|
6997
|
-
// --- Format-derived artifact strip (R7.2) ---
|
|
6998
|
-
// #770 A1: real deliverables (artifact_set / pull_request / deck / report /
|
|
6999
|
-
// markdown) render in the Deliverables panel (renderDeliverablesPanel), not
|
|
7000
|
-
// pinned in the thread.
|
|
7001
|
-
|
|
7002
|
-
// --- Unified action bar: review actions appear beside the always-on chips ---
|
|
7003
|
-
actions.hidden = false;
|
|
7004
|
-
renderDynamicReviewActions(conv, reviewHandoffForConversation(conv));
|
|
7005
|
-
const handoff = els['review-handoff'];
|
|
7006
|
-
if (handoff) {
|
|
7007
|
-
const hasArtifact = !!(fmt.actions && fmt.actions.length);
|
|
7008
|
-
handoff.textContent = fmt.key === 'pull_request'
|
|
7009
|
-
? 'Comment on the PR to leave inline notes, or approve / type changes below.'
|
|
7010
|
-
: isManagerTemplateJob(conv && conv.jobId)
|
|
7011
|
-
? 'Open the updated files, then approve or type changes below.'
|
|
7012
|
-
: hasArtifact
|
|
7013
|
-
? 'Open the deliverable above, then approve or type changes below.'
|
|
7014
|
-
: 'Review the update above, then approve or type changes below.';
|
|
6986
|
+
document.getElementById('review-action-storage').appendChild(actions);
|
|
7015
6987
|
}
|
|
7016
6988
|
}
|
|
7017
6989
|
|
|
@@ -7232,34 +7204,46 @@ function approvalCommandFromLabel(action) {
|
|
|
7232
7204
|
return label ? `Approved and ${label}.` : APPROVAL_MESSAGE;
|
|
7233
7205
|
}
|
|
7234
7206
|
|
|
7235
|
-
//
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
const
|
|
7207
|
+
// Run outputs stay reachable independently of the current review decision.
|
|
7208
|
+
function renderDeliverablesBar(conv) {
|
|
7209
|
+
const messages = els['messages'];
|
|
7210
|
+
if (!messages) return;
|
|
7211
|
+
let panel = messages.querySelector('#deliverables-bar');
|
|
7212
|
+
if (!panel) {
|
|
7213
|
+
panel = document.createElement('div');
|
|
7214
|
+
panel.id = 'deliverables-bar';
|
|
7215
|
+
panel.setAttribute('aria-label', 'Deliverables');
|
|
7216
|
+
}
|
|
7217
|
+
const body = panel;
|
|
7218
|
+
let fmt = conv ? deriveDeliverableFormat(conv) : null;
|
|
7219
|
+
// A later run snapshot may clear its handoff; produced outputs remain available.
|
|
7220
|
+
// Historical data supplies artifact links only, never decision eligibility.
|
|
7221
|
+
if (conv && (!reviewHandoffForConversation(conv) || reviewHandoffForConversation(conv).reviewRequired === false) && (!fmt || !fmt.actions.length)) {
|
|
7222
|
+
const previous = (conv.reviewTurns || []).at(-1);
|
|
7223
|
+
if (previous) {
|
|
7224
|
+
try { fmt = deriveFormatFromReviewHandoff(normalizeReviewHandoff(JSON.parse(previous.signature))); }
|
|
7225
|
+
catch { /* Ignore malformed history from older/imported records. */ }
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7246
7228
|
const isDeliverableFmt = !!fmt && (fmt.key === 'artifact_set' || fmt.key === 'pull_request'
|
|
7247
7229
|
|| fmt.key === 'deck' || fmt.key === 'report'
|
|
7248
7230
|
|| (fmt.key === 'markdown' && fmt.actions && fmt.actions.length));
|
|
7249
7231
|
const actions = isDeliverableFmt ? fmt.actions.filter((a) => a.kind !== 'feedback') : [];
|
|
7250
|
-
// #770 / #512 R7.2: a handoff that EXPLICITLY declines review (reviewRequired:false,
|
|
7251
|
-
// e.g. org-onboarding wrote in-place context files reviewed via the Company/Manager
|
|
7252
|
-
// Brief tab) is not a deliverable to surface here. This preserves the prior "no
|
|
7253
|
-
// artifact strip for a no-review handoff" behavior now that the strip is decoupled
|
|
7254
|
-
// from the (hidden) review card.
|
|
7255
7232
|
const currentHandoff = conv ? reviewHandoffForConversation(conv) : null;
|
|
7256
|
-
const declinedReview = !!currentHandoff && currentHandoff.reviewRequired === false;
|
|
7233
|
+
const declinedReview = !!currentHandoff && currentHandoff.reviewRequired === false && !(conv.reviewTurns || []).length;
|
|
7257
7234
|
if (!conv || declinedReview || actions.length === 0) {
|
|
7258
7235
|
panel.hidden = true;
|
|
7236
|
+
delete panel.dataset.signature;
|
|
7259
7237
|
body.innerHTML = '';
|
|
7260
7238
|
return;
|
|
7261
7239
|
}
|
|
7262
7240
|
panel.hidden = false;
|
|
7241
|
+
const signature = JSON.stringify(actions);
|
|
7242
|
+
if (panel.dataset.signature === signature) {
|
|
7243
|
+
if (messages.lastElementChild !== panel) messages.appendChild(panel);
|
|
7244
|
+
return;
|
|
7245
|
+
}
|
|
7246
|
+
panel.dataset.signature = signature;
|
|
7263
7247
|
body.innerHTML = '';
|
|
7264
7248
|
const strip = document.createElement('div');
|
|
7265
7249
|
strip.className = 'rc-artifact-strip';
|
|
@@ -7292,13 +7276,7 @@ function renderDeliverablesPanel(conv) {
|
|
|
7292
7276
|
strip.appendChild(btn);
|
|
7293
7277
|
}
|
|
7294
7278
|
body.appendChild(strip);
|
|
7295
|
-
|
|
7296
|
-
if (summary) {
|
|
7297
|
-
summary.textContent = actions.length === 1 ? '1 file from this run' : `${actions.length} files from this run`;
|
|
7298
|
-
}
|
|
7299
|
-
const panelState = panelStateFor(conv.id);
|
|
7300
|
-
const shouldOpen = panelState.deliverables !== undefined ? panelState.deliverables : convAwaitingReview(conv);
|
|
7301
|
-
if (panel.open !== shouldOpen) panel.open = shouldOpen;
|
|
7279
|
+
messages.appendChild(panel);
|
|
7302
7280
|
}
|
|
7303
7281
|
|
|
7304
7282
|
function jobCatalogEntry(jobId) {
|
|
@@ -7829,6 +7807,7 @@ async function approveReview(options) {
|
|
|
7829
7807
|
const selectedReviewAction = opts.reviewAction && typeof opts.reviewAction === 'object'
|
|
7830
7808
|
? opts.reviewAction
|
|
7831
7809
|
: { id: 'approve', label: 'Approve', kind: 'approve', style: 'primary' };
|
|
7810
|
+
resolveReviewTurn(conv, 'You approved this.');
|
|
7832
7811
|
conv.reviewApproved = true;
|
|
7833
7812
|
conv.selectedReviewAction = selectedReviewAction;
|
|
7834
7813
|
conv.pendingDeliveryActionId = selectedReviewAction.deliveryActionId || null;
|
|
@@ -7845,6 +7824,7 @@ async function approveReview(options) {
|
|
|
7845
7824
|
await continueRun(approvalMessage, { preserveReviewApproved: true, selectedReviewAction });
|
|
7846
7825
|
return;
|
|
7847
7826
|
}
|
|
7827
|
+
resolveReviewTurn(conv, 'You marked this complete.');
|
|
7848
7828
|
conv.status = 'completed';
|
|
7849
7829
|
conv.reviewApproved = false;
|
|
7850
7830
|
conv.pendingDeliveryActionId = null;
|
|
@@ -7864,6 +7844,8 @@ async function approveReview(options) {
|
|
|
7864
7844
|
async function requestChangesReview() {
|
|
7865
7845
|
const conv = activeConversation();
|
|
7866
7846
|
if (!conv) return;
|
|
7847
|
+
if (!convAwaitingReview(conv)) return;
|
|
7848
|
+
resolveReviewTurn(conv, 'You asked for changes.');
|
|
7867
7849
|
const typedText = (els['coach-text'] && els['coach-text'].value.trim()) || '';
|
|
7868
7850
|
const text = typedText || DEFAULT_REQUEST_CHANGES_MESSAGE;
|
|
7869
7851
|
// Re-open the review gate: a change request means the run is no longer "done".
|
|
@@ -8553,7 +8535,7 @@ function showRerunToast(msg) {
|
|
|
8553
8535
|
const t = document.createElement('div');
|
|
8554
8536
|
t.className = 'cp-rerun-toast';
|
|
8555
8537
|
// Issue #1627: matches this app's own convention for transient status text (#be-status,
|
|
8556
|
-
//
|
|
8538
|
+
// status notifications, etc.) so a screen reader announces the message instead of it being silently
|
|
8557
8539
|
// visual-only - relevant beyond the original re-run case now that this toast also carries the
|
|
8558
8540
|
// real reason a restart-to-latest attempt failed.
|
|
8559
8541
|
t.setAttribute('role', 'status');
|
|
@@ -9087,6 +9069,7 @@ function conversationDispatchAgentId(conv) {
|
|
|
9087
9069
|
async function continueRun(text, options) {
|
|
9088
9070
|
const conv = activeConversation();
|
|
9089
9071
|
if (!conv || !conv.runId) return;
|
|
9072
|
+
if (convAwaitingReview(conv)) resolveReviewTurn(conv, 'You replied instead. The run continued without a decision.');
|
|
9090
9073
|
const opts = options || {};
|
|
9091
9074
|
// Consume any pending coaching job selected via quick-coach or template picker.
|
|
9092
9075
|
// The server adds the correct invocation prefix ($fraim / /fraim) based on
|
|
@@ -9205,6 +9188,7 @@ async function continueRun(text, options) {
|
|
|
9205
9188
|
}
|
|
9206
9189
|
|
|
9207
9190
|
function foldRunIntoConversation(conv, run) {
|
|
9191
|
+
syncReviewTurns(conv);
|
|
9208
9192
|
const createdAt = stableConversationCreatedAt(conv, run);
|
|
9209
9193
|
if (createdAt !== undefined) conv.createdAt = createdAt;
|
|
9210
9194
|
// Issue #710: server run identity is canonical. The browser may have
|
|
@@ -9505,9 +9489,47 @@ function switchToConversation(id) {
|
|
|
9505
9489
|
// Status line + project picker
|
|
9506
9490
|
// ---------------------------------------------------------------------------
|
|
9507
9491
|
|
|
9492
|
+
let statusNotificationTimer;
|
|
9493
|
+
function positionStatusNotification() {
|
|
9494
|
+
const notice = document.getElementById('hub-status-notification');
|
|
9495
|
+
const composer = document.getElementById('persistent-composer');
|
|
9496
|
+
if (!notice || notice.hidden) return;
|
|
9497
|
+
const box = composer && composer.getBoundingClientRect();
|
|
9498
|
+
const visible = box && box.width > 0 && box.top > 0 && box.top < innerHeight;
|
|
9499
|
+
notice.style.bottom = (visible ? innerHeight - box.top + 8 : 16) + 'px';
|
|
9500
|
+
notice.style.left = (visible ? box.left + box.width / 2 : innerWidth / 2) + 'px';
|
|
9501
|
+
}
|
|
9508
9502
|
function showStatus(text, isError) {
|
|
9509
|
-
|
|
9510
|
-
|
|
9503
|
+
clearTimeout(statusNotificationTimer);
|
|
9504
|
+
let notice = document.getElementById('hub-status-notification');
|
|
9505
|
+
if (!notice) {
|
|
9506
|
+
notice = document.createElement('div');
|
|
9507
|
+
notice.id = 'hub-status-notification';
|
|
9508
|
+
document.body.appendChild(notice);
|
|
9509
|
+
window.addEventListener('resize', positionStatusNotification);
|
|
9510
|
+
document.addEventListener('scroll', positionStatusNotification, true);
|
|
9511
|
+
const composer = document.getElementById('persistent-composer');
|
|
9512
|
+
if (composer && typeof ResizeObserver !== 'undefined') new ResizeObserver(positionStatusNotification).observe(composer);
|
|
9513
|
+
}
|
|
9514
|
+
notice.className = 'cp-rerun-toast hub-status-notification' + (isError ? ' error' : '');
|
|
9515
|
+
notice.replaceChildren();
|
|
9516
|
+
notice.hidden = !text;
|
|
9517
|
+
notice.setAttribute('role', isError ? 'alert' : 'status');
|
|
9518
|
+
notice.setAttribute('aria-live', isError ? 'assertive' : 'polite');
|
|
9519
|
+
const message = document.createElement('span');
|
|
9520
|
+
message.textContent = text || '';
|
|
9521
|
+
notice.appendChild(message);
|
|
9522
|
+
if (isError) {
|
|
9523
|
+
const dismiss = document.createElement('button');
|
|
9524
|
+
dismiss.type = 'button';
|
|
9525
|
+
dismiss.setAttribute('aria-label', 'Dismiss notification');
|
|
9526
|
+
dismiss.textContent = '\u00d7';
|
|
9527
|
+
dismiss.onclick = () => { notice.hidden = true; };
|
|
9528
|
+
notice.appendChild(dismiss);
|
|
9529
|
+
} else {
|
|
9530
|
+
statusNotificationTimer = setTimeout(() => { notice.hidden = true; }, 5000);
|
|
9531
|
+
}
|
|
9532
|
+
positionStatusNotification();
|
|
9511
9533
|
}
|
|
9512
9534
|
|
|
9513
9535
|
async function pickProject() {
|
|
@@ -10418,25 +10440,7 @@ function wireEvents() {
|
|
|
10418
10440
|
els['micro-manage-toggle'].focus();
|
|
10419
10441
|
scrollThreadForReview(activeConversation());
|
|
10420
10442
|
});
|
|
10421
|
-
|
|
10422
|
-
els['thread-panel'].addEventListener('toggle', () => {
|
|
10423
|
-
const conv = activeConversation();
|
|
10424
|
-
if (!conv) return;
|
|
10425
|
-
panelStateFor(conv.id).thread = els['thread-panel'].open;
|
|
10426
|
-
if (els['thread-panel'].open) requestAnimationFrame(() => restoreManagePosition(conv));
|
|
10427
|
-
});
|
|
10428
|
-
}
|
|
10429
|
-
|
|
10430
|
-
// #770: Deliverables panel toggle: persist open/close state per conversation.
|
|
10431
|
-
if (els['deliverables-panel']) {
|
|
10432
|
-
els['deliverables-panel'].addEventListener('toggle', () => {
|
|
10433
|
-
const conv = activeConversation();
|
|
10434
|
-
if (conv) panelStateFor(conv.id).deliverables = els['deliverables-panel'].open;
|
|
10435
|
-
});
|
|
10436
|
-
}
|
|
10437
|
-
|
|
10438
|
-
// Issue #512 R7.7: unified action-bar review buttons. They live in the same
|
|
10439
|
-
// bar as the coaching chips; the chips/input never disappear.
|
|
10443
|
+
// The live review controls retain their handlers when moved into a review turn.
|
|
10440
10444
|
if (els['review-approve']) {
|
|
10441
10445
|
els['review-approve'].addEventListener('click', () => approveReview({ resumeAfterApproval: true }));
|
|
10442
10446
|
}
|
|
@@ -10551,18 +10555,6 @@ function wireEvents() {
|
|
|
10551
10555
|
return;
|
|
10552
10556
|
}
|
|
10553
10557
|
});
|
|
10554
|
-
if (els['conversation-focus-btn']) {
|
|
10555
|
-
els['conversation-focus-btn'].addEventListener('click', openConversationFocus);
|
|
10556
|
-
}
|
|
10557
|
-
if (els['conversation-focus-close']) {
|
|
10558
|
-
els['conversation-focus-close'].addEventListener('click', closeConversationFocus);
|
|
10559
|
-
}
|
|
10560
|
-
if (els['conversation-focus-overlay']) {
|
|
10561
|
-
els['conversation-focus-overlay'].addEventListener('click', (e) => {
|
|
10562
|
-
if (e.target === els['conversation-focus-overlay']) closeConversationFocus();
|
|
10563
|
-
});
|
|
10564
|
-
}
|
|
10565
|
-
document.addEventListener('keydown', handleConversationFocusEscape);
|
|
10566
10558
|
|
|
10567
10559
|
// Issue #1657: #coach-text's own keydown handler (wired above, alongside
|
|
10568
10560
|
// arrow-key suggestion navigation) now owns Enter-to-send directly via
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
/* z-index layers (ascending, avoid magic numbers in property declarations). */
|
|
48
48
|
--z-cmd-palette: 9000;
|
|
49
49
|
--z-dropdown-menu: 9100;
|
|
50
|
-
/* Job-viz modal must clear #composer-suggestions
|
|
50
|
+
/* Job-viz modal must clear #composer-suggestions
|
|
51
51
|
(--z-cmd-palette). The #jv-tt tooltip portal (--z-jv-tt) must clear the
|
|
52
52
|
modal so phase/skill popovers float above it when the modal is open. */
|
|
53
53
|
--z-job-viz: 9200;
|
|
@@ -910,35 +910,6 @@ img.conv-employee-avatar {
|
|
|
910
910
|
}
|
|
911
911
|
.run-stop-btn:hover { background: #fee4e2; }
|
|
912
912
|
.run-stop-btn:disabled { opacity: .55; cursor: default; }
|
|
913
|
-
.conversation-focus-btn {
|
|
914
|
-
flex-shrink: 0;
|
|
915
|
-
display: inline-flex;
|
|
916
|
-
align-items: center;
|
|
917
|
-
justify-content: center;
|
|
918
|
-
width: 32px;
|
|
919
|
-
height: 32px;
|
|
920
|
-
font-size: 17px;
|
|
921
|
-
font-weight: 700;
|
|
922
|
-
line-height: 1;
|
|
923
|
-
color: var(--accent-strong);
|
|
924
|
-
background: var(--accent-soft);
|
|
925
|
-
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
|
|
926
|
-
border-radius: 8px;
|
|
927
|
-
padding: 0;
|
|
928
|
-
cursor: pointer;
|
|
929
|
-
}
|
|
930
|
-
.panel-details--thread .conversation-focus-btn {
|
|
931
|
-
margin-left: auto;
|
|
932
|
-
}
|
|
933
|
-
.conversation-focus-btn:hover {
|
|
934
|
-
border-color: var(--accent);
|
|
935
|
-
color: var(--accent);
|
|
936
|
-
}
|
|
937
|
-
.conversation-focus-btn:focus-visible,
|
|
938
|
-
.conversation-focus-close:focus-visible {
|
|
939
|
-
outline: 2px solid var(--accent);
|
|
940
|
-
outline-offset: 2px;
|
|
941
|
-
}
|
|
942
913
|
/* Job title inline with employee avatar — replaces the old separate h2 row */
|
|
943
914
|
.conv-job-title {
|
|
944
915
|
flex: 1;
|
|
@@ -2401,14 +2372,7 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
|
|
|
2401
2372
|
}
|
|
2402
2373
|
.resume-command-copy:hover { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
2403
2374
|
|
|
2404
|
-
|
|
2405
|
-
margin: 0;
|
|
2406
|
-
color: var(--muted);
|
|
2407
|
-
font-size: 13px;
|
|
2408
|
-
text-align: center;
|
|
2409
|
-
min-height: 1.2em;
|
|
2410
|
-
}
|
|
2411
|
-
.status-line.error { color: var(--danger); }
|
|
2375
|
+
|
|
2412
2376
|
|
|
2413
2377
|
/* Modal */
|
|
2414
2378
|
.modal-backdrop {
|
|
@@ -4216,37 +4180,22 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4216
4180
|
.workspace-conv #active-conv > .conv-topline,
|
|
4217
4181
|
.workspace-conv #active-conv > .conversation-status { flex: 0 0 auto; }
|
|
4218
4182
|
|
|
4219
|
-
/*
|
|
4220
|
-
|
|
4221
|
-
to fill spare height; collapsed it is just its summary bar. */
|
|
4222
|
-
.workspace-conv #thread-panel { flex: 0 0 auto; min-height: 0; }
|
|
4223
|
-
/* Open thread is the sole space-filler: it grows to take all height the support
|
|
4224
|
-
stack does not need, and scrolls #messages internally. Collapsed it is just
|
|
4225
|
-
its summary bar. */
|
|
4226
|
-
.workspace-conv #thread-panel[open] {
|
|
4183
|
+
/* The static Manage section fills spare height and scrolls messages internally. */
|
|
4184
|
+
.workspace-conv #thread-panel {
|
|
4227
4185
|
flex: 1 1 0;
|
|
4228
4186
|
display: flex;
|
|
4229
4187
|
flex-direction: column;
|
|
4230
4188
|
min-height: 120px;
|
|
4231
4189
|
}
|
|
4232
|
-
.workspace-conv #thread-panel >
|
|
4233
|
-
/*
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
.workspace-conv #thread-panel[open]::details-content {
|
|
4237
|
-
flex: 1 1 0;
|
|
4238
|
-
min-height: 0;
|
|
4239
|
-
display: flex;
|
|
4240
|
-
flex-direction: column;
|
|
4241
|
-
overflow: hidden;
|
|
4242
|
-
}
|
|
4243
|
-
.workspace-conv #thread-panel[open] #messages {
|
|
4190
|
+
.workspace-conv #thread-panel > .thread-header { flex: 0 0 auto; }
|
|
4191
|
+
/* The conversation body shrinks inside the static Manage section. */
|
|
4192
|
+
|
|
4193
|
+
.workspace-conv #thread-panel #messages {
|
|
4244
4194
|
flex: 1 1 0;
|
|
4245
4195
|
min-height: 0;
|
|
4246
4196
|
max-height: none;
|
|
4247
4197
|
overflow-y: auto;
|
|
4248
4198
|
}
|
|
4249
|
-
.workspace-conv #thread-panel:not([open]) #messages { display: none; }
|
|
4250
4199
|
|
|
4251
4200
|
/* Both Manage views use the same flex slot; switching does not resize the pane. */
|
|
4252
4201
|
#thread-panel .manage-body {
|
|
@@ -4257,7 +4206,6 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4257
4206
|
min-width: 0;
|
|
4258
4207
|
overflow: hidden;
|
|
4259
4208
|
}
|
|
4260
|
-
#thread-panel:not([open]) .manage-body { display: none; }
|
|
4261
4209
|
#thread-panel .manage-activity {
|
|
4262
4210
|
display: flex;
|
|
4263
4211
|
flex-direction: column;
|
|
@@ -4280,10 +4228,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4280
4228
|
|
|
4281
4229
|
/* Keep independent header controls outside the native disclosure button. */
|
|
4282
4230
|
.manage-panel-shell { position: relative; display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; min-width: 0; }
|
|
4283
|
-
.manage-panel-shell:has(#thread-panel:not([open])) { flex: 0 0 auto; }
|
|
4284
4231
|
.manage-header-actions { position: absolute; top: 12px; right: 24px; display: flex; align-items: center; gap: 10px; }
|
|
4285
|
-
#thread-panel >
|
|
4286
|
-
#thread-panel >
|
|
4232
|
+
#thread-panel > .thread-header { gap: 10px; min-height: 60px; padding-right: 190px; box-sizing: border-box; }
|
|
4233
|
+
#thread-panel > .thread-header .panel-summary-copy { margin-right: auto; }
|
|
4287
4234
|
.micro-manage-toggle {
|
|
4288
4235
|
display: inline-flex;
|
|
4289
4236
|
align-items: center;
|
|
@@ -4308,8 +4255,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4308
4255
|
.micro-activity-state, .micro-return { margin: 0 16px 12px; color: var(--text); }
|
|
4309
4256
|
.micro-return { align-self: flex-start; padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); cursor: pointer; }
|
|
4310
4257
|
@media (max-width: 480px) {
|
|
4311
|
-
#thread-panel >
|
|
4312
|
-
.manage-header-actions .conversation-focus-btn { order: 1; }
|
|
4258
|
+
#thread-panel > .thread-header { flex-wrap: wrap; }
|
|
4313
4259
|
.manage-header-actions { right: 24px; }
|
|
4314
4260
|
}
|
|
4315
4261
|
|
|
@@ -4359,14 +4305,6 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4359
4305
|
overflow-y: auto;
|
|
4360
4306
|
}
|
|
4361
4307
|
|
|
4362
|
-
/* #770: Deliverables panel sizes to its content and scrolls internally when open,
|
|
4363
|
-
mirroring the coach/micro support-stack panels. */
|
|
4364
|
-
.workspace-conv .support-stack > .panel-details--deliverables { flex: 0 0 auto; }
|
|
4365
|
-
.workspace-conv .support-stack > .panel-details--deliverables[open] > .panel-body {
|
|
4366
|
-
max-height: clamp(120px, 22vh, 240px);
|
|
4367
|
-
overflow-y: auto;
|
|
4368
|
-
}
|
|
4369
|
-
|
|
4370
4308
|
.workspace-conv #active-conv.has-delegation-board .delegation-ledger[open] .delegation-ledger-body {
|
|
4371
4309
|
max-height: clamp(140px, 24vh, 260px);
|
|
4372
4310
|
}
|
|
@@ -4391,125 +4329,6 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4391
4329
|
.workspace-conv .support-stack > .panel-details,
|
|
4392
4330
|
.workspace-conv .support-stack > .micro { margin-bottom: 0; }
|
|
4393
4331
|
|
|
4394
|
-
.conversation-focus-overlay {
|
|
4395
|
-
position: fixed;
|
|
4396
|
-
inset: 0;
|
|
4397
|
-
z-index: var(--z-cmd-palette);
|
|
4398
|
-
display: flex;
|
|
4399
|
-
align-items: stretch;
|
|
4400
|
-
justify-content: center;
|
|
4401
|
-
padding: 18px;
|
|
4402
|
-
background: rgba(14, 21, 32, 0.42);
|
|
4403
|
-
overflow: hidden;
|
|
4404
|
-
}
|
|
4405
|
-
.conversation-focus-overlay[hidden] {
|
|
4406
|
-
display: none;
|
|
4407
|
-
}
|
|
4408
|
-
.conversation-focus-dialog {
|
|
4409
|
-
width: min(1180px, 100%);
|
|
4410
|
-
height: 100%;
|
|
4411
|
-
min-width: 0;
|
|
4412
|
-
min-height: 0;
|
|
4413
|
-
display: flex;
|
|
4414
|
-
flex-direction: column;
|
|
4415
|
-
gap: 10px;
|
|
4416
|
-
border: 1px solid var(--line);
|
|
4417
|
-
border-radius: 10px;
|
|
4418
|
-
background: var(--surface);
|
|
4419
|
-
box-shadow: var(--shadow-lg);
|
|
4420
|
-
padding: 12px;
|
|
4421
|
-
overflow: hidden;
|
|
4422
|
-
}
|
|
4423
|
-
.conversation-focus-header {
|
|
4424
|
-
display: flex;
|
|
4425
|
-
align-items: center;
|
|
4426
|
-
justify-content: space-between;
|
|
4427
|
-
gap: 12px;
|
|
4428
|
-
flex: 0 0 auto;
|
|
4429
|
-
min-width: 0;
|
|
4430
|
-
}
|
|
4431
|
-
.conversation-focus-copy {
|
|
4432
|
-
display: grid;
|
|
4433
|
-
gap: 2px;
|
|
4434
|
-
min-width: 0;
|
|
4435
|
-
}
|
|
4436
|
-
.conversation-focus-copy strong {
|
|
4437
|
-
color: var(--text);
|
|
4438
|
-
font-size: 15px;
|
|
4439
|
-
}
|
|
4440
|
-
.conversation-focus-copy span {
|
|
4441
|
-
color: var(--muted);
|
|
4442
|
-
font-size: 12px;
|
|
4443
|
-
overflow: hidden;
|
|
4444
|
-
text-overflow: ellipsis;
|
|
4445
|
-
white-space: nowrap;
|
|
4446
|
-
}
|
|
4447
|
-
.conversation-focus-close {
|
|
4448
|
-
flex-shrink: 0;
|
|
4449
|
-
border: 1px solid var(--line);
|
|
4450
|
-
border-radius: 8px;
|
|
4451
|
-
background: var(--surface);
|
|
4452
|
-
color: var(--text);
|
|
4453
|
-
font-size: 12px;
|
|
4454
|
-
font-weight: 700;
|
|
4455
|
-
padding: 7px 11px;
|
|
4456
|
-
cursor: pointer;
|
|
4457
|
-
}
|
|
4458
|
-
.conversation-focus-close:hover {
|
|
4459
|
-
border-color: var(--accent);
|
|
4460
|
-
color: var(--accent-strong);
|
|
4461
|
-
}
|
|
4462
|
-
.conversation-focus-panels {
|
|
4463
|
-
flex: 1 1 0;
|
|
4464
|
-
min-height: 0;
|
|
4465
|
-
min-width: 0;
|
|
4466
|
-
display: flex;
|
|
4467
|
-
flex-direction: column;
|
|
4468
|
-
gap: 8px;
|
|
4469
|
-
overflow: hidden;
|
|
4470
|
-
}
|
|
4471
|
-
.conversation-focus-panels #thread-panel {
|
|
4472
|
-
margin: 0;
|
|
4473
|
-
min-height: 0;
|
|
4474
|
-
}
|
|
4475
|
-
.conversation-focus-panels #thread-panel[open] {
|
|
4476
|
-
flex: 1 1 0;
|
|
4477
|
-
display: flex;
|
|
4478
|
-
flex-direction: column;
|
|
4479
|
-
}
|
|
4480
|
-
.conversation-focus-panels #thread-panel[open]::details-content {
|
|
4481
|
-
flex: 1 1 0;
|
|
4482
|
-
min-height: 0;
|
|
4483
|
-
display: flex;
|
|
4484
|
-
flex-direction: column;
|
|
4485
|
-
overflow: hidden;
|
|
4486
|
-
}
|
|
4487
|
-
.conversation-focus-panels #thread-panel[open] #messages {
|
|
4488
|
-
flex: 1 1 0;
|
|
4489
|
-
min-height: 0;
|
|
4490
|
-
max-height: none;
|
|
4491
|
-
overflow-y: auto;
|
|
4492
|
-
}
|
|
4493
|
-
.conversation-focus-panels #persistent-composer .coach {
|
|
4494
|
-
min-height: 0;
|
|
4495
|
-
}
|
|
4496
|
-
.conversation-focus-panels #persistent-composer .coach-input textarea {
|
|
4497
|
-
min-height: 68px;
|
|
4498
|
-
}
|
|
4499
|
-
.conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer,
|
|
4500
|
-
.conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach,
|
|
4501
|
-
.conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach-input {
|
|
4502
|
-
flex: 1 1 0;
|
|
4503
|
-
}
|
|
4504
|
-
.conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach {
|
|
4505
|
-
display: flex;
|
|
4506
|
-
flex-direction: column;
|
|
4507
|
-
}
|
|
4508
|
-
.conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach-input textarea {
|
|
4509
|
-
flex: 1 1 0;
|
|
4510
|
-
min-height: 160px;
|
|
4511
|
-
}
|
|
4512
|
-
|
|
4513
4332
|
@media (max-width: 820px) {
|
|
4514
4333
|
/* On narrow screens drop the fill model: panels flow naturally and the page
|
|
4515
4334
|
scrolls, with each open body keeping a viewport-relative scroll cap. */
|
|
@@ -4518,7 +4337,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4518
4337
|
flex-direction: column;
|
|
4519
4338
|
overflow: visible;
|
|
4520
4339
|
}
|
|
4521
|
-
.workspace-conv #thread-panel
|
|
4340
|
+
.workspace-conv #thread-panel,
|
|
4522
4341
|
.workspace-conv .support-stack,
|
|
4523
4342
|
.workspace-conv .support-stack:has(.micro[open]),
|
|
4524
4343
|
.workspace-conv .support-stack > .micro[open] {
|
|
@@ -4526,7 +4345,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4526
4345
|
display: block;
|
|
4527
4346
|
min-height: 0;
|
|
4528
4347
|
}
|
|
4529
|
-
.workspace-conv #thread-panel
|
|
4348
|
+
.workspace-conv #thread-panel #messages,
|
|
4530
4349
|
.workspace-conv .support-stack > .micro[open] > .micro-log {
|
|
4531
4350
|
flex: initial;
|
|
4532
4351
|
height: auto;
|
|
@@ -4541,22 +4360,6 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4541
4360
|
|
|
4542
4361
|
@media (max-width: 640px) {
|
|
4543
4362
|
body.hub-shell { overflow-x: hidden; }
|
|
4544
|
-
.conversation-focus-overlay {
|
|
4545
|
-
padding: 8px;
|
|
4546
|
-
}
|
|
4547
|
-
.conversation-focus-dialog {
|
|
4548
|
-
border-radius: 8px;
|
|
4549
|
-
padding: 8px;
|
|
4550
|
-
}
|
|
4551
|
-
.conversation-focus-header {
|
|
4552
|
-
align-items: flex-start;
|
|
4553
|
-
}
|
|
4554
|
-
.conversation-focus-copy span {
|
|
4555
|
-
white-space: normal;
|
|
4556
|
-
}
|
|
4557
|
-
.conversation-focus-close {
|
|
4558
|
-
padding: 7px 9px;
|
|
4559
|
-
}
|
|
4560
4363
|
.gs-rail { overflow-x: auto; padding-inline: 10px; }
|
|
4561
4364
|
.hub-tabs { min-width: 0; }
|
|
4562
4365
|
.htab { padding-inline: 14px; }
|
|
@@ -4605,9 +4408,9 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4605
4408
|
}
|
|
4606
4409
|
|
|
4607
4410
|
@media (max-width: 820px) {
|
|
4608
|
-
.workspace-conv #thread-panel
|
|
4609
|
-
.workspace-conv #thread-panel
|
|
4610
|
-
.workspace-conv #thread-panel
|
|
4411
|
+
.workspace-conv #thread-panel { max-height: none; }
|
|
4412
|
+
.workspace-conv #thread-panel .manage-body { height: clamp(320px, 44vh, 420px); flex: none; }
|
|
4413
|
+
.workspace-conv #thread-panel .manage-body #messages { flex: 1 1 0; height: auto; max-height: none; }
|
|
4611
4414
|
}
|
|
4612
4415
|
|
|
4613
4416
|
/* Review surfaces */
|
|
@@ -6855,7 +6658,7 @@ img.eh-av { object-fit: cover; background: var(--surface); }
|
|
|
6855
6658
|
/* ─── End Issue #1178 ────────────────────────────────────────────────────── */
|
|
6856
6659
|
|
|
6857
6660
|
/* ─── Issue #1278: Job Visualization Modal ──────────────────────────────── */
|
|
6858
|
-
/* Must sit above #composer-suggestions
|
|
6661
|
+
/* Must sit above #composer-suggestions
|
|
6859
6662
|
(var(--z-cmd-palette) = 9000) so the modal is reachable when opened from
|
|
6860
6663
|
the persistent composer's catalog suggestions. Token: --z-job-viz. */
|
|
6861
6664
|
#job-viz-modal { z-index: var(--z-job-viz); }
|
|
@@ -6950,7 +6753,7 @@ img.eh-av { object-fit: cover; background: var(--surface); }
|
|
|
6950
6753
|
siblings get flex:1 so whichever is visible fills the remaining height and
|
|
6951
6754
|
pushes the composer to the true bottom edge. */
|
|
6952
6755
|
#empty { flex: 1; min-height: 0; overflow-y: auto; }
|
|
6953
|
-
#conversation:has(> #persistent-composer) { padding-bottom:
|
|
6756
|
+
#conversation:has(> #persistent-composer) { padding-bottom: 0; }
|
|
6954
6757
|
#persistent-composer {
|
|
6955
6758
|
flex-shrink: 0;
|
|
6956
6759
|
display: flex;
|
|
@@ -7096,3 +6899,31 @@ img.eh-av { object-fit: cover; background: var(--surface); }
|
|
|
7096
6899
|
}
|
|
7097
6900
|
#cp-armed-job-clear:hover { color: var(--text); }
|
|
7098
6901
|
/* ─── End Issue #1657 Treatment 3 ────────────────────────────────────────── */
|
|
6902
|
+
|
|
6903
|
+
/* Conversation panes remain open; only the transcript owns the scrolling area. */
|
|
6904
|
+
.thread-header { display: flex; align-items: center; padding: 12px 16px; flex: 0 0 auto; }
|
|
6905
|
+
#thread-panel > .thread-header { min-height: 48px; padding-right: 176px; }
|
|
6906
|
+
.manage-header-actions { top: 6px; right: 16px; }
|
|
6907
|
+
#conversation:has(> #persistent-composer) { padding-bottom: 0; }
|
|
6908
|
+
#run-state-pill[role="button"] { cursor: pointer; }
|
|
6909
|
+
#run-state-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
6910
|
+
#deliverables-bar { position: sticky; bottom: 0; z-index: 2; flex: 0 0 auto; margin-top: auto; padding: 10px; background: var(--surface); border-top: 1px solid var(--line); }
|
|
6911
|
+
#deliverables-bar[hidden], .hub-status-notification[hidden] { display: none; }
|
|
6912
|
+
#deliverables-bar .rc-art-btn:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
6913
|
+
#deliverables-bar .rc-art-btn { overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box; }
|
|
6914
|
+
.review-turn { flex: 0 0 auto; }
|
|
6915
|
+
.review-turn .bubble { min-width: 0; max-width: 100%; }
|
|
6916
|
+
.review-turn-label { font-size: 11px; font-weight: 700; color: var(--muted); margin-bottom: 8px; }
|
|
6917
|
+
.review-resolution { font-size: 13px; color: var(--muted); }
|
|
6918
|
+
.hub-status-notification { pointer-events: auto; display: flex; align-items: center; gap: 12px; max-width: min(480px, calc(100vw - 32px)); box-sizing: border-box; white-space: normal; overflow-wrap: anywhere; }
|
|
6919
|
+
.hub-status-notification.error { border: 1px solid var(--danger); }
|
|
6920
|
+
.hub-status-notification button { flex: 0 0 auto; background: transparent; color: inherit; border: 0; cursor: pointer; font-size: 20px; }
|
|
6921
|
+
.hub-status-notification button:focus-visible { outline: 2px solid currentColor; }
|
|
6922
|
+
#ab-direct-thread-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
6923
|
+
@media (max-width: 480px) {
|
|
6924
|
+
#deliverables-bar .rc-artifact-strip { flex-direction: column; }
|
|
6925
|
+
#deliverables-bar { max-height: 160px; overflow-y: auto; }
|
|
6926
|
+
.review-turn .review-actions { flex-direction: column; align-items: stretch; }
|
|
6927
|
+
}
|
|
6928
|
+
|
|
6929
|
+
:root[data-theme="dark"] #deliverables-bar .rc-art-btn:not(.ghost) { color: var(--bg); }
|