fraim-hub 2.0.323 → 2.0.325
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/server.js +6 -4
- package/package.json +2 -2
- package/public/ai-hub/index.html +17 -18
- package/public/ai-hub/review.css +4 -0
- package/public/ai-hub/script.js +101 -12
- package/public/ai-hub/styles.css +66 -1
|
@@ -7189,9 +7189,10 @@ class AiHubServer {
|
|
|
7189
7189
|
// job that just received coaching has not reached retrospective
|
|
7190
7190
|
// again yet. Clear it at send-time so a coached Done job stops
|
|
7191
7191
|
// offering next-job recommendations immediately, not only once the
|
|
7192
|
-
// resumed session's next seekMentoring signal arrives.
|
|
7192
|
+
// resumed session's next seekMentoring signal arrives. The active
|
|
7193
|
+
// reviewHandoff deliberately survives conversational coaching; its
|
|
7194
|
+
// lifecycle is owned by applySeekMentoringSignal (issue #1763).
|
|
7193
7195
|
current.nextJobRecommendations = null;
|
|
7194
|
-
current.reviewHandoff = null;
|
|
7195
7196
|
});
|
|
7196
7197
|
const startedFresh = this.runRegistry.get(run.id);
|
|
7197
7198
|
if (startedFresh)
|
|
@@ -7260,9 +7261,10 @@ class AiHubServer {
|
|
|
7260
7261
|
current.pendingDeliveryActionId = selectedReviewAction.deliveryActionId || null;
|
|
7261
7262
|
}
|
|
7262
7263
|
// Issue #1373 (Defect 2): see the no-session branch above for why
|
|
7263
|
-
//
|
|
7264
|
+
// next-job recommendations can only be stale at coaching-send time.
|
|
7265
|
+
// applySeekMentoringSignal remains the sole reviewHandoff lifecycle
|
|
7266
|
+
// owner so a conversational coaching turn does not decide the review.
|
|
7264
7267
|
current.nextJobRecommendations = null;
|
|
7265
|
-
current.reviewHandoff = null;
|
|
7266
7268
|
});
|
|
7267
7269
|
const started = this.runRegistry.get(run.id);
|
|
7268
7270
|
if (started)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.325",
|
|
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",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"electron-updater": "^6.8.9",
|
|
214
214
|
"express": "^5.2.1",
|
|
215
215
|
"extract-zip": "^2.0.1",
|
|
216
|
-
"fraim": "2.0.
|
|
216
|
+
"fraim": "2.0.325",
|
|
217
217
|
"mongodb": "^7.0.0",
|
|
218
218
|
"node-cron": "4.2.1",
|
|
219
219
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -439,9 +439,10 @@
|
|
|
439
439
|
<details class="panel-details panel-details--thread" id="thread-panel" open>
|
|
440
440
|
<summary>
|
|
441
441
|
<span class="panel-summary-copy">
|
|
442
|
-
<span class="panel-kicker" id="thread-panel-kicker">
|
|
442
|
+
<span class="panel-kicker" id="thread-panel-kicker">Manage</span>
|
|
443
443
|
</span>
|
|
444
444
|
<button class="conversation-focus-btn" id="conversation-focus-btn" type="button" aria-label="Expand conversation" title="Expand conversation" onclick="event.preventDefault(); event.stopPropagation();">↗</button>
|
|
445
|
+
<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>
|
|
445
446
|
</summary>
|
|
446
447
|
<!-- Issue #1065: find a passage inside the open conversation (R44-R46). A result that
|
|
447
448
|
promises "+6 more matches" is an unkept promise without this: the manager lands in
|
|
@@ -458,7 +459,21 @@
|
|
|
458
459
|
<!-- Issue #512 R7: completion card + format-derived artifact strip
|
|
459
460
|
render INSIDE #messages (as the employee's review turn) so the
|
|
460
461
|
thread panel keeps its existing flex/scroll geometry. -->
|
|
461
|
-
<div class="
|
|
462
|
+
<div class="manage-body" id="manage-body">
|
|
463
|
+
<div class="messages" id="messages"></div>
|
|
464
|
+
<div class="manage-activity" id="micro-manage" hidden>
|
|
465
|
+
<button class="micro-return" id="micro-return" type="button" hidden>Return to conversation</button>
|
|
466
|
+
<div class="micro-activity-state" id="micro-activity-state" role="status" hidden></div>
|
|
467
|
+
<pre class="micro-log" id="micro-log" tabindex="0" aria-label="Detailed activity"></pre>
|
|
468
|
+
<div class="resume-command" id="resume-command" hidden>
|
|
469
|
+
<span class="resume-command-label">Resume in terminal</span>
|
|
470
|
+
<div class="resume-command-row">
|
|
471
|
+
<pre class="resume-command-code" id="resume-command-code"></pre>
|
|
472
|
+
<button class="resume-command-copy" id="resume-command-copy" type="button" title="Copy command">Copy</button>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
462
477
|
</details>
|
|
463
478
|
|
|
464
479
|
<div class="support-stack">
|
|
@@ -480,22 +495,6 @@
|
|
|
480
495
|
active coaching actions cannot hide it. -->
|
|
481
496
|
<div class="agent-unavailable-note" id="active-agent-unavailable-note" data-testid="active-agent-unavailable-note" role="status" hidden></div>
|
|
482
497
|
|
|
483
|
-
<details class="micro panel-details" id="micro-manage">
|
|
484
|
-
<summary>
|
|
485
|
-
<span class="panel-summary-copy">
|
|
486
|
-
<span class="panel-kicker">Micro-manage</span>
|
|
487
|
-
<span class="panel-summary-text">Raw host events and tool calls</span>
|
|
488
|
-
</span>
|
|
489
|
-
</summary>
|
|
490
|
-
<pre class="micro-log" id="micro-log"></pre>
|
|
491
|
-
<div class="resume-command" id="resume-command" hidden>
|
|
492
|
-
<span class="resume-command-label">Resume in terminal</span>
|
|
493
|
-
<div class="resume-command-row">
|
|
494
|
-
<pre class="resume-command-code" id="resume-command-code"></pre>
|
|
495
|
-
<button class="resume-command-copy" id="resume-command-copy" type="button" title="Copy command">Copy</button>
|
|
496
|
-
</div>
|
|
497
|
-
</div>
|
|
498
|
-
</details>
|
|
499
498
|
</div>
|
|
500
499
|
</div>
|
|
501
500
|
|
package/public/ai-hub/review.css
CHANGED
|
@@ -140,6 +140,10 @@
|
|
|
140
140
|
color: var(--muted, #888);
|
|
141
141
|
filter: none;
|
|
142
142
|
}
|
|
143
|
+
:root[data-theme="dark"] .review-actions .rb-approve:not(:disabled):not(.managed-disabled),
|
|
144
|
+
:root[data-theme="dark"] .review-actions .rb-secondary:not(:disabled):not(.managed-disabled) {
|
|
145
|
+
color: var(--bg, #1e1e1e);
|
|
146
|
+
}
|
|
143
147
|
.review-actions .rb-handoff {
|
|
144
148
|
font-size: 12px;
|
|
145
149
|
color: var(--muted);
|
package/public/ai-hub/script.js
CHANGED
|
@@ -134,7 +134,7 @@ function gatherElements() {
|
|
|
134
134
|
'status-line', 'coach-note',
|
|
135
135
|
// Issue #1651: composer attachments.
|
|
136
136
|
'coach', 'composer-dropzone', 'staged-attachments', 'attach-btn', 'attach-file-input',
|
|
137
|
-
'thread-panel', 'thread-panel-kicker',
|
|
137
|
+
'thread-panel', 'thread-panel-kicker', 'micro-manage-toggle', 'micro-return', 'micro-activity-state',
|
|
138
138
|
'quick-coach-btns',
|
|
139
139
|
'positive-reinforcement-btn', 'positive-reinforcement-dropdown',
|
|
140
140
|
'negative-reinforcement-btn', 'negative-reinforcement-dropdown',
|
|
@@ -1417,7 +1417,8 @@ async function hydrateConversationBody(projectPath, id, options = {}) {
|
|
|
1417
1417
|
try {
|
|
1418
1418
|
const payload = await requestJson(`/api/ai-hub/conversations?${params.toString()}`);
|
|
1419
1419
|
const body = payload && payload.conversation;
|
|
1420
|
-
if (!body)
|
|
1420
|
+
if (!body) throw new Error('Conversation activity is unavailable');
|
|
1421
|
+
activityLoadErrors.delete(manageKey(body));
|
|
1421
1422
|
normalizeGeminiConversationMessages(body);
|
|
1422
1423
|
const current = findConversation(body.id);
|
|
1423
1424
|
// A body hydrate can be in flight while a live run poll or continue POST
|
|
@@ -1439,6 +1440,7 @@ async function hydrateConversationBody(projectPath, id, options = {}) {
|
|
|
1439
1440
|
return current;
|
|
1440
1441
|
}
|
|
1441
1442
|
const merged = mergeConversationBody(current, body);
|
|
1443
|
+
activityLoadErrors.delete(manageKey(merged));
|
|
1442
1444
|
const bucket = convBucketKey(merged);
|
|
1443
1445
|
const list = (state.conversations[bucket] || []).slice();
|
|
1444
1446
|
const idx = list.findIndex((conv) => conv.id === merged.id);
|
|
@@ -1453,6 +1455,11 @@ async function hydrateConversationBody(projectPath, id, options = {}) {
|
|
|
1453
1455
|
return merged;
|
|
1454
1456
|
} catch (error) {
|
|
1455
1457
|
console.warn('Could not hydrate conversation body:', error);
|
|
1458
|
+
const failed = findConversation(id);
|
|
1459
|
+
if (failed) {
|
|
1460
|
+
activityLoadErrors.add(manageKey(failed));
|
|
1461
|
+
if (state.activeId === failed.id) syncManageActivityState(failed, failed.microEvents || failed.events || []);
|
|
1462
|
+
}
|
|
1456
1463
|
return null;
|
|
1457
1464
|
} finally {
|
|
1458
1465
|
conversationBodyFetches.delete(key);
|
|
@@ -2069,12 +2076,11 @@ function managerLabel() {
|
|
|
2069
2076
|
return id.name.split(' ')[0];
|
|
2070
2077
|
}
|
|
2071
2078
|
|
|
2072
|
-
//
|
|
2079
|
+
// Manage is stable across agents and both levels of detail.
|
|
2073
2080
|
function syncThreadPanelKicker(conv) {
|
|
2074
2081
|
const el = els['thread-panel-kicker'] || document.getElementById('thread-panel-kicker');
|
|
2075
2082
|
if (!el) return;
|
|
2076
|
-
|
|
2077
|
-
el.textContent = name ? `${name} updates` : 'Updates';
|
|
2083
|
+
el.textContent = 'Manage';
|
|
2078
2084
|
}
|
|
2079
2085
|
|
|
2080
2086
|
// Returns the FRAIM invocation prefix for the currently active employee:
|
|
@@ -2276,6 +2282,60 @@ function formatEmployeeText(text) {
|
|
|
2276
2282
|
return out.join('\n');
|
|
2277
2283
|
}
|
|
2278
2284
|
|
|
2285
|
+
// Presentation state is session-only and belongs to the record's own scope.
|
|
2286
|
+
const manageViews = new Map();
|
|
2287
|
+
const activityLoadErrors = new Set();
|
|
2288
|
+
let displayedManageKey = null;
|
|
2289
|
+
function manageKey(conv) { return conv ? `${convBucketKey(conv)}:${conv.id}` : null; }
|
|
2290
|
+
function manageViewFor(conv) {
|
|
2291
|
+
const key = manageKey(conv);
|
|
2292
|
+
if (!manageViews.has(key)) manageViews.set(key, { micro: false, messages: { top: 0, follow: true }, activity: { top: 0, follow: true } });
|
|
2293
|
+
return manageViews.get(key);
|
|
2294
|
+
}
|
|
2295
|
+
function captureManagePosition() {
|
|
2296
|
+
const view = manageViews.get(displayedManageKey);
|
|
2297
|
+
if (!view || !els['thread-panel'].open) return;
|
|
2298
|
+
const node = view.micro ? els['micro-log'] : els['messages'];
|
|
2299
|
+
if (!node || !node.clientHeight) return;
|
|
2300
|
+
view[view.micro ? 'activity' : 'messages'] = {
|
|
2301
|
+
top: node.scrollTop,
|
|
2302
|
+
follow: node.scrollHeight - node.scrollTop - node.clientHeight < 80,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
function restoreManagePosition(conv) {
|
|
2306
|
+
if (displayedManageKey !== manageKey(conv)) return;
|
|
2307
|
+
const view = manageViewFor(conv);
|
|
2308
|
+
const position = view[view.micro ? 'activity' : 'messages'];
|
|
2309
|
+
const node = view.micro ? els['micro-log'] : els['messages'];
|
|
2310
|
+
node.scrollTo({ top: position.follow ? node.scrollHeight : position.top, behavior: 'instant' });
|
|
2311
|
+
}
|
|
2312
|
+
function applyManageView(conv) {
|
|
2313
|
+
const micro = !!conv && conv.compareMode !== 'ab' && manageViewFor(conv).micro;
|
|
2314
|
+
els['micro-manage-toggle'].hidden = !conv || conv.compareMode === 'ab';
|
|
2315
|
+
els['micro-manage-toggle'].setAttribute('aria-checked', String(micro));
|
|
2316
|
+
els['micro-manage'].hidden = !micro;
|
|
2317
|
+
els['messages'].hidden = micro;
|
|
2318
|
+
if (els['conv-find']) els['conv-find'].hidden = micro || !convFind.open;
|
|
2319
|
+
}
|
|
2320
|
+
function setManageMicroMode(enabled) {
|
|
2321
|
+
const conv = activeConversation();
|
|
2322
|
+
if (!conv || conv.compareMode === 'ab') return;
|
|
2323
|
+
captureManagePosition();
|
|
2324
|
+
manageViewFor(conv).micro = enabled;
|
|
2325
|
+
els['thread-panel'].open = true;
|
|
2326
|
+
applyManageView(conv);
|
|
2327
|
+
restoreManagePosition(conv);
|
|
2328
|
+
}
|
|
2329
|
+
function syncManageActivityState(conv, events) {
|
|
2330
|
+
const error = activityLoadErrors.has(manageKey(conv));
|
|
2331
|
+
const message = error ? 'Activity is unavailable. You can return to the conversation.'
|
|
2332
|
+
: !conversationHasBody(conv) ? 'Loading activity…' : !events.length ? 'No activity yet' : '';
|
|
2333
|
+
els['micro-activity-state'].textContent = message;
|
|
2334
|
+
els['micro-activity-state'].hidden = !message;
|
|
2335
|
+
// Keep an explicit way back to prompts even when the detailed view owns the body.
|
|
2336
|
+
els['micro-return'].hidden = !(error || conversationUiState(conv) === 'waiting' || convAwaitingReview(conv));
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2279
2339
|
function syncConversationPanels(conv, switchedConv) {
|
|
2280
2340
|
if (!conv) return;
|
|
2281
2341
|
const panelState = panelStateFor(conv.id);
|
|
@@ -2326,6 +2386,8 @@ async function deleteConversation(id) {
|
|
|
2326
2386
|
if (next.length !== list.length) { state.conversations[key] = next; removed = true; }
|
|
2327
2387
|
}
|
|
2328
2388
|
if (!removed) return false;
|
|
2389
|
+
manageViews.delete(manageKey(target));
|
|
2390
|
+
activityLoadErrors.delete(manageKey(target));
|
|
2329
2391
|
if (state.activeId === id) {
|
|
2330
2392
|
state.activeId = null;
|
|
2331
2393
|
// Deleting the active run: stop its poll so no zombie interval keeps ticking
|
|
@@ -3768,6 +3830,7 @@ function restoreFocusedConversationNodes() {
|
|
|
3768
3830
|
|
|
3769
3831
|
function closeConversationFocus() {
|
|
3770
3832
|
if (!conversationFocusState.open) return;
|
|
3833
|
+
captureManagePosition();
|
|
3771
3834
|
if (els['messages']) conversationFocusState.messageScrollTop = els['messages'].scrollTop;
|
|
3772
3835
|
restoreFocusedConversationNodes();
|
|
3773
3836
|
conversationFocusState.open = false;
|
|
@@ -3779,6 +3842,8 @@ function closeConversationFocus() {
|
|
|
3779
3842
|
requestAnimationFrame(() => {
|
|
3780
3843
|
if (els['messages']) els['messages'].scrollTop = conversationFocusState.messageScrollTop || 0;
|
|
3781
3844
|
if (typeof syncThreadMessageViewport === 'function') syncThreadMessageViewport();
|
|
3845
|
+
const conv = activeConversation();
|
|
3846
|
+
if (conv) restoreManagePosition(conv);
|
|
3782
3847
|
const target = conversationFocusState.previousFocus && document.contains(conversationFocusState.previousFocus)
|
|
3783
3848
|
? conversationFocusState.previousFocus
|
|
3784
3849
|
: els['conversation-focus-btn'];
|
|
@@ -3788,6 +3853,7 @@ function closeConversationFocus() {
|
|
|
3788
3853
|
|
|
3789
3854
|
function openConversationFocus() {
|
|
3790
3855
|
const conv = activeConversation();
|
|
3856
|
+
captureManagePosition();
|
|
3791
3857
|
const overlay = els['conversation-focus-overlay'];
|
|
3792
3858
|
const panels = els['conversation-focus-panels'];
|
|
3793
3859
|
const thread = els['thread-panel'];
|
|
@@ -3815,6 +3881,7 @@ function openConversationFocus() {
|
|
|
3815
3881
|
requestAnimationFrame(() => {
|
|
3816
3882
|
if (els['messages']) els['messages'].scrollTop = conversationFocusState.messageScrollTop || 0;
|
|
3817
3883
|
if (typeof syncThreadMessageViewport === 'function') syncThreadMessageViewport();
|
|
3884
|
+
restoreManagePosition(conv);
|
|
3818
3885
|
if (els['conversation-focus-close']) els['conversation-focus-close'].focus();
|
|
3819
3886
|
});
|
|
3820
3887
|
}
|
|
@@ -3881,6 +3948,11 @@ function renderActive() {
|
|
|
3881
3948
|
// If we switched conversations (or this is the first render), wipe and
|
|
3882
3949
|
// start fresh. Otherwise we're going to do an incremental update below.
|
|
3883
3950
|
const switchedConv = renderedConvId !== conv.id;
|
|
3951
|
+
if (displayedManageKey !== manageKey(conv)) {
|
|
3952
|
+
captureManagePosition();
|
|
3953
|
+
displayedManageKey = manageKey(conv);
|
|
3954
|
+
}
|
|
3955
|
+
applyManageView(conv);
|
|
3884
3956
|
if (switchedConv) {
|
|
3885
3957
|
els['messages'].innerHTML = '';
|
|
3886
3958
|
els['messages'].className = 'messages';
|
|
@@ -4055,8 +4127,11 @@ function renderActive() {
|
|
|
4055
4127
|
// <pre> wipes the entire log every tick which is wasteful.
|
|
4056
4128
|
const microEvents = Array.isArray(conv.microEvents) && conv.microEvents.length > 0 ? conv.microEvents : null;
|
|
4057
4129
|
const events = microEvents || conv.events || [];
|
|
4130
|
+
if (!switchedConv && !els['micro-manage'].hidden) captureManagePosition();
|
|
4058
4131
|
renderMicroLogEvents(events, { mode: microEvents ? 'microEvents' : 'events' });
|
|
4059
4132
|
renderResumeCommand(conv);
|
|
4133
|
+
syncManageActivityState(conv, events);
|
|
4134
|
+
if (!els['micro-manage'].hidden || switchedConv) requestAnimationFrame(() => restoreManagePosition(conv));
|
|
4060
4135
|
|
|
4061
4136
|
// Coaching state — only enable Send when there's text and the run is resumable.
|
|
4062
4137
|
syncSendButton();
|
|
@@ -4075,7 +4150,7 @@ function renderActive() {
|
|
|
4075
4150
|
// marks a manager is stepping through. Re-emit them from find state instead of injecting once.
|
|
4076
4151
|
if (typeof convFindRefresh === 'function') convFindRefresh();
|
|
4077
4152
|
const shouldScrollForUpdate = switchedConv || statusChanged || appendedMessages > 0 || messagesMutated;
|
|
4078
|
-
const reviewFollow = switchedConv || statusChanged || threadWasNearBottom;
|
|
4153
|
+
const reviewFollow = (switchedConv || statusChanged || threadWasNearBottom) && (!switchedConv || manageViewFor(conv).messages.follow);
|
|
4079
4154
|
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
|
|
4080
4155
|
requestAnimationFrame(() => {
|
|
4081
4156
|
syncThreadMessageViewport();
|
|
@@ -5709,6 +5784,7 @@ function ensurePendingAnchorScrollListener(host) {
|
|
|
5709
5784
|
}
|
|
5710
5785
|
|
|
5711
5786
|
function setThreadScrollTop(host, top) {
|
|
5787
|
+
if (host.hidden) return;
|
|
5712
5788
|
pendingAnchorProgrammaticScrollUntil = Date.now() + 200;
|
|
5713
5789
|
host.scrollTop = top;
|
|
5714
5790
|
}
|
|
@@ -5721,7 +5797,7 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom,
|
|
|
5721
5797
|
const latest = activeConversation();
|
|
5722
5798
|
if (!latest || latest.id !== conv.id) return;
|
|
5723
5799
|
const host = els['messages'];
|
|
5724
|
-
if (!host) return;
|
|
5800
|
+
if (!host || host.hidden) return;
|
|
5725
5801
|
if (latest.status === 'running') {
|
|
5726
5802
|
ensurePendingAnchorScrollListener(host);
|
|
5727
5803
|
if (!forceBottom && pendingAnchorUserScrolled(host)) {
|
|
@@ -7637,15 +7713,17 @@ async function approveReview(options) {
|
|
|
7637
7713
|
const opts = options || {};
|
|
7638
7714
|
const conv = activeConversation();
|
|
7639
7715
|
if (!conv) return;
|
|
7716
|
+
const shouldResume = !!opts.resumeAfterApproval
|
|
7717
|
+
&& !!conv.runId
|
|
7718
|
+
&& convAwaitingReview(conv);
|
|
7719
|
+
if (opts.resumeAfterApproval && !shouldResume) {
|
|
7720
|
+
showStatus('Approve could not resume this run. The review gate is no longer active. Use Mark complete if you intend to close it manually.', true);
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7640
7723
|
if (state.pollHandle) {
|
|
7641
7724
|
window.clearInterval(state.pollHandle);
|
|
7642
7725
|
state.pollHandle = null;
|
|
7643
7726
|
}
|
|
7644
|
-
const shouldResume = !!opts.resumeAfterApproval
|
|
7645
|
-
&& !!conv.runId
|
|
7646
|
-
&& !!conv.sessionId
|
|
7647
|
-
&& !!reviewHandoffForConversation(conv)
|
|
7648
|
-
&& convAwaitingReview(conv);
|
|
7649
7727
|
if (shouldResume) {
|
|
7650
7728
|
const selectedReviewAction = opts.reviewAction && typeof opts.reviewAction === 'object'
|
|
7651
7729
|
? opts.reviewAction
|
|
@@ -10234,6 +10312,16 @@ function wireEvents() {
|
|
|
10234
10312
|
}
|
|
10235
10313
|
|
|
10236
10314
|
// R6: thread panel toggle — persist open/close state per conversation.
|
|
10315
|
+
els['micro-manage-toggle'].addEventListener('click', (event) => {
|
|
10316
|
+
event.preventDefault();
|
|
10317
|
+
event.stopPropagation();
|
|
10318
|
+
setManageMicroMode(els['micro-manage-toggle'].getAttribute('aria-checked') !== 'true');
|
|
10319
|
+
});
|
|
10320
|
+
els['micro-return'].addEventListener('click', () => {
|
|
10321
|
+
setManageMicroMode(false);
|
|
10322
|
+
els['micro-manage-toggle'].focus();
|
|
10323
|
+
scrollThreadForReview(activeConversation());
|
|
10324
|
+
});
|
|
10237
10325
|
if (els['thread-panel']) {
|
|
10238
10326
|
els['thread-panel'].addEventListener('toggle', () => {
|
|
10239
10327
|
const conv = activeConversation();
|
|
@@ -11325,6 +11413,7 @@ function convFindOpen(term) {
|
|
|
11325
11413
|
if (!bar || !input) return;
|
|
11326
11414
|
const cleaned = convFindTextFromQuery(term);
|
|
11327
11415
|
if (!cleaned) return;
|
|
11416
|
+
setManageMicroMode(false);
|
|
11328
11417
|
bar.hidden = false;
|
|
11329
11418
|
convFind.open = true;
|
|
11330
11419
|
input.value = cleaned;
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -4246,7 +4246,66 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4246
4246
|
}
|
|
4247
4247
|
.workspace-conv #thread-panel:not([open]) #messages { display: none; }
|
|
4248
4248
|
|
|
4249
|
-
/*
|
|
4249
|
+
/* Both Manage views use the same flex slot; switching does not resize the pane. */
|
|
4250
|
+
#thread-panel .manage-body {
|
|
4251
|
+
display: flex;
|
|
4252
|
+
flex-direction: column;
|
|
4253
|
+
flex: 1 1 0;
|
|
4254
|
+
min-height: 0;
|
|
4255
|
+
min-width: 0;
|
|
4256
|
+
overflow: hidden;
|
|
4257
|
+
}
|
|
4258
|
+
#thread-panel:not([open]) .manage-body { display: none; }
|
|
4259
|
+
#thread-panel .manage-activity {
|
|
4260
|
+
display: flex;
|
|
4261
|
+
flex-direction: column;
|
|
4262
|
+
flex: 1 1 0;
|
|
4263
|
+
min-height: 0;
|
|
4264
|
+
min-width: 0;
|
|
4265
|
+
overflow: hidden;
|
|
4266
|
+
}
|
|
4267
|
+
#thread-panel .manage-activity[hidden],
|
|
4268
|
+
#thread-panel #messages[hidden] { display: none; }
|
|
4269
|
+
#thread-panel .manage-activity .micro-log {
|
|
4270
|
+
flex: 1 1 0;
|
|
4271
|
+
min-height: 0;
|
|
4272
|
+
max-height: 100%;
|
|
4273
|
+
overflow: auto;
|
|
4274
|
+
white-space: pre;
|
|
4275
|
+
}
|
|
4276
|
+
#thread-panel .resume-command { flex: 0 0 auto; }
|
|
4277
|
+
#thread-panel > summary { gap: 10px; }
|
|
4278
|
+
#thread-panel > summary .panel-summary-copy { margin-right: auto; }
|
|
4279
|
+
.micro-manage-toggle {
|
|
4280
|
+
display: inline-flex;
|
|
4281
|
+
align-items: center;
|
|
4282
|
+
justify-content: center;
|
|
4283
|
+
gap: 8px;
|
|
4284
|
+
flex-shrink: 0;
|
|
4285
|
+
min-height: 36px;
|
|
4286
|
+
border: 0;
|
|
4287
|
+
background: transparent;
|
|
4288
|
+
color: var(--text);
|
|
4289
|
+
font: inherit;
|
|
4290
|
+
font-size: 12px;
|
|
4291
|
+
cursor: pointer;
|
|
4292
|
+
white-space: nowrap;
|
|
4293
|
+
}
|
|
4294
|
+
.micro-manage-toggle[hidden] { display: none; }
|
|
4295
|
+
.micro-switch-track { width: 30px; height: 18px; border: 1px solid var(--muted); border-radius: 12px; background: var(--line); }
|
|
4296
|
+
.micro-switch-track::after { content: ''; display: block; width: 12px; height: 12px; margin: 2px; border-radius: 50%; background: var(--muted); }
|
|
4297
|
+
.micro-manage-toggle[aria-checked="true"] .micro-switch-track { background: var(--accent); border-color: var(--accent); }
|
|
4298
|
+
.micro-manage-toggle[aria-checked="true"] .micro-switch-track::after { transform: translateX(12px); background: white; }
|
|
4299
|
+
.micro-manage-toggle:focus-visible, .micro-return:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
4300
|
+
.micro-activity-state, .micro-return { margin: 0 16px 12px; color: var(--text); }
|
|
4301
|
+
.micro-return { align-self: flex-start; padding: 6px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); cursor: pointer; }
|
|
4302
|
+
@media (max-width: 480px) {
|
|
4303
|
+
#thread-panel > summary { flex-wrap: wrap; }
|
|
4304
|
+
#thread-panel > summary .conversation-focus-btn { order: 1; margin-left: auto; }
|
|
4305
|
+
#thread-panel > summary .micro-manage-toggle { margin-left: auto; }
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
/* Support stack: coaching controls. It sizes to its content and
|
|
4250
4309
|
never grows past it, so the thread keeps the spare real estate. Each open body
|
|
4251
4310
|
has a viewport-relative cap and scrolls internally, so it shows more on bigger
|
|
4252
4311
|
screens (and far more than the old 1-line peek) without crowding the thread. */
|
|
@@ -4537,6 +4596,12 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
|
|
|
4537
4596
|
}
|
|
4538
4597
|
}
|
|
4539
4598
|
|
|
4599
|
+
@media (max-width: 820px) {
|
|
4600
|
+
.workspace-conv #thread-panel[open] { max-height: none; }
|
|
4601
|
+
.workspace-conv #thread-panel[open] .manage-body { height: clamp(180px, 36vh, 320px); flex: none; }
|
|
4602
|
+
.workspace-conv #thread-panel[open] .manage-body #messages { flex: 1 1 0; height: auto; max-height: none; }
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4540
4605
|
/* Review surfaces */
|
|
4541
4606
|
.completion-card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; margin-bottom: 8px; }
|
|
4542
4607
|
.completion-card .cc-title { font-weight: 700; margin-bottom: 6px; font-size: 14px; }
|