fraim-hub 2.0.321 → 2.0.322

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.
@@ -16,7 +16,6 @@ const defaultPreferences = (projectPath) => ({
16
16
  employeeId: DEFAULT_EMPLOYEE,
17
17
  categoryId: DEFAULT_CATEGORY,
18
18
  recentJobIds: [],
19
- recentJobInstructions: {},
20
19
  personaKey: null,
21
20
  projects: normalizeAiHubProjectList([], projectPath),
22
21
  removedProjectPaths: [],
@@ -179,9 +178,6 @@ class AiHubPreferencesStore {
179
178
  employeeId: (raw.employeeId === 'claude' || raw.employeeId === 'codex' || raw.employeeId === 'gemini' || raw.employeeId === 'copilot' || raw.employeeId === 'antigravity') ? raw.employeeId : DEFAULT_EMPLOYEE,
180
179
  categoryId: typeof raw.categoryId === 'string' && raw.categoryId.length > 0 ? raw.categoryId : DEFAULT_CATEGORY,
181
180
  recentJobIds: Array.isArray(raw.recentJobIds) ? raw.recentJobIds.filter((value) => typeof value === 'string') : [],
182
- recentJobInstructions: (typeof raw.recentJobInstructions === 'object' && raw.recentJobInstructions !== null && !Array.isArray(raw.recentJobInstructions))
183
- ? raw.recentJobInstructions
184
- : {},
185
181
  personaKey: typeof raw.personaKey === 'string' ? raw.personaKey : null,
186
182
  // Issue #750: apiKey is no longer a persisted field here at all — any
187
183
  // legacy apiKey left over from a pre-#750 ai-hub-state.json is silently
@@ -232,18 +228,13 @@ class AiHubPreferencesStore {
232
228
  this.save({ ...preferences, projectPath: normalizedProjectPath, projects: nextProjects, removedProjectPaths });
233
229
  return nextProjects;
234
230
  }
235
- remember(preferences, jobId, instructions) {
231
+ remember(preferences, jobId) {
236
232
  const recentJobIds = jobId
237
233
  ? [jobId, ...preferences.recentJobIds.filter((value) => value !== jobId)].slice(0, 8)
238
234
  : preferences.recentJobIds.slice(0, 8);
239
- const recentJobInstructions = { ...preferences.recentJobInstructions };
240
- if (jobId && instructions) {
241
- recentJobInstructions[jobId] = instructions.slice(0, 500);
242
- }
243
235
  const next = {
244
236
  ...preferences,
245
237
  recentJobIds,
246
- recentJobInstructions,
247
238
  };
248
239
  this.save(next);
249
240
  return next;
@@ -7020,7 +7020,7 @@ class AiHubServer {
7020
7020
  projectPath: isProjectIndependentRun ? existingPreferences.projectPath : projectPath,
7021
7021
  employeeId: hostId,
7022
7022
  recentJobIds: existingPreferences.recentJobIds,
7023
- }, jobId, typeof instructions === 'string' ? instructions : undefined);
7023
+ }, jobId);
7024
7024
  const fraimRunEnriched = this.enrichRunForResponse(this.runRegistry.get(run.id) ?? run);
7025
7025
  const responsePayload = directRun
7026
7026
  ? { ...fraimRunEnriched, compareRun: this.enrichRunForResponse(directRun) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.321",
3
+ "version": "2.0.322",
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",
@@ -212,7 +212,7 @@
212
212
  "electron-updater": "^6.8.9",
213
213
  "express": "^5.2.1",
214
214
  "extract-zip": "^2.0.1",
215
- "fraim": "2.0.321",
215
+ "fraim": "2.0.322",
216
216
  "mongodb": "^7.0.0",
217
217
  "node-cron": "4.2.1",
218
218
  "node-edge-tts": "^1.2.10",
@@ -425,7 +425,6 @@
425
425
  <div class="conv-topline">
426
426
  <div class="employee-identity" id="active-identity"></div>
427
427
  <span id="active-title" class="conv-job-title"></span>
428
- <button class="run-stop-btn" id="run-stop-btn" type="button" hidden title="Ask the employee to stop and wait for you">⏹ Stop</button>
429
428
  <div class="run-state-pill" id="run-state-pill"></div>
430
429
  </div>
431
430
 
@@ -477,112 +476,9 @@
477
476
  </details>
478
477
  <!-- Issue #1292: when this conversation's own agent stops being available,
479
478
  say so. The selector keeps holding that agent; it is never quietly
480
- swapped for another one. Lives outside #coach-panel so a collapsed
481
- coaching panel cannot hide it. -->
479
+ swapped for another one. Lives outside the composer controls so
480
+ active coaching actions cannot hide it. -->
482
481
  <div class="agent-unavailable-note" id="active-agent-unavailable-note" data-testid="active-agent-unavailable-note" role="status" hidden></div>
483
- <details class="panel-details panel-details--coach" id="coach-panel" open>
484
- <summary>
485
- <span class="panel-summary-copy">
486
- <span class="panel-kicker" id="coach-panel-kicker">Your coaching</span>
487
- <span class="panel-summary-text" id="coach-summary">Open when you want to steer the next step</span>
488
- </span>
489
- <span class="active-employee-row" onclick="event.stopPropagation()">
490
- <span class="active-employee-label">Maestro</span>
491
- <select id="active-employee-select" class="employee-select inline" aria-label="Agent Tool"></select>
492
- </span>
493
- </summary>
494
- <div class="panel-body">
495
- <!-- Issue #512 R7.7: review actions live in the SAME bar as the
496
- coaching chips. Hidden until a deliverable is awaiting review;
497
- the coaching chips below never disappear. -->
498
- <div class="review-actions" id="review-actions" hidden>
499
- <button class="rb-approve" type="button" id="review-approve">✓ Approve</button>
500
- <button class="rb-secondary" type="button" id="review-request-changes">↩ Request changes</button>
501
- <span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
502
- </div>
503
- <div class="quick-coach-row" id="quick-coach-btns" hidden>
504
- <button class="ghost quick-coach-btn mark-complete-btn" type="button" id="mark-complete-btn" aria-label="Mark the job complete">✓ Mark complete</button>
505
- <!-- R4: Positive Reinforcement dropdown -->
506
- <div class="coach-dropdown" id="positive-reinforcement-wrap">
507
- <button class="ghost quick-coach-btn" type="button" id="positive-reinforcement-btn" data-testid="positive-reinforcement-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="positive-reinforcement-dropdown">Positive Reinforcement ▾</button>
508
- <div class="coach-dropdown-menu" id="positive-reinforcement-dropdown" data-testid="positive-reinforcement-dropdown" hidden role="menu">
509
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
510
- data-job="recognize-good-work"
511
- title="Capture what went well so the employee repeats it">Well done</button>
512
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
513
- data-job="praise-fraim"
514
- title="Record praise for the employee in the FRAIM registry">Praise publicly</button>
515
- </div>
516
- </div>
517
- <!-- R5: Negative Reinforcement dropdown -->
518
- <div class="coach-dropdown" id="negative-reinforcement-wrap">
519
- <button class="ghost quick-coach-btn" type="button" id="negative-reinforcement-btn" data-testid="negative-reinforcement-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="negative-reinforcement-dropdown">Negative Reinforcement ▾</button>
520
- <div class="coach-dropdown-menu" id="negative-reinforcement-dropdown" data-testid="negative-reinforcement-dropdown" hidden role="menu">
521
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
522
- data-job="analyze-why-you-messed-up"
523
- title="Ask the employee to diagnose the root cause and what to change next time">Analyze your mistake</button>
524
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
525
- data-job="follow-your-mentor"
526
- title="Ask the employee to re-read the workflow instructions and continue from the correct next step">Follow due process</button>
527
- </div>
528
- </div>
529
- <!-- R6: Conviction dropdown (label is dynamic, set by syncConvictionDropdownLabel) -->
530
- <div class="coach-dropdown" id="conviction-wrap">
531
- <button class="ghost quick-coach-btn" type="button" id="conviction-btn" data-testid="conviction-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="conviction-dropdown">Your conviction ▾</button>
532
- <div class="coach-dropdown-menu" id="conviction-dropdown" data-testid="conviction-dropdown" hidden role="menu">
533
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
534
- data-job="strong-pov"
535
- title="Ask the employee to defend a strong, well-reasoned position">Strong pov</button>
536
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
537
- data-job="weak-pov"
538
- title="Ask the employee to acknowledge uncertainty and present a hedged view">Weak pov</button>
539
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
540
- data-job="need-pov"
541
- title="Ask the employee to develop a point of view where they currently have none">Need pov</button>
542
- </div>
543
- </div>
544
- <!-- Issue #1069: Explain it dropdown -->
545
- <div class="coach-dropdown" id="explain-wrap">
546
- <button class="ghost quick-coach-btn" type="button" id="explain-btn" data-testid="explain-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="explain-dropdown">Explain it &#9662;</button>
547
- <div class="coach-dropdown-menu" id="explain-dropdown" data-testid="explain-dropdown" hidden role="menu">
548
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
549
- data-job="explain-it"
550
- data-explain-mode="simpler"
551
- data-prompt="Explain this more simply."
552
- title="Ask for a shorter explanation with less jargon">Simpler</button>
553
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
554
- data-job="explain-it"
555
- data-explain-mode="business"
556
- data-prompt="Explain this in business terms."
557
- title="Ask for impact, tradeoffs, and decision context">Business terms</button>
558
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
559
- data-job="explain-it"
560
- data-explain-mode="technical"
561
- data-prompt="Explain this in technical terms."
562
- title="Ask for implementation details and technical caveats">Technical terms</button>
563
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
564
- data-job="explain-it"
565
- data-explain-mode="five-year-old"
566
- data-prompt="Explain this like I am 5."
567
- title="Ask for a very simple explanation">Like a 5 year old</button>
568
- <button class="ghost quick-coach-btn" type="button" role="menuitem"
569
- data-job="explain-it"
570
- data-explain-mode="preferred"
571
- data-prompt="Explain this the way I like it."
572
- title="Ask the employee to use your learned explanation preferences">The way I like it</button>
573
- </div>
574
- </div>
575
- </div>
576
- <!-- Issue #1176: visible only while the run is actively WORKING (R34/R42).
577
- Two-option toggle, not a native radiogroup, matching the approved UI
578
- mock (docs/feature-specs/mocks/1176-continue-run-guidance.html). -->
579
- <div class="delivery-control" id="delivery-control" role="group" aria-label="When should this message run?" hidden>
580
- <button class="delivery-option" type="button" id="delivery-after" aria-pressed="true">After current</button>
581
- <button class="delivery-option delivery-option--redirect" type="button" id="delivery-redirect" aria-pressed="false">Redirect now</button>
582
- </div>
583
- <p class="delivery-help" id="delivery-help" hidden></p>
584
- </div>
585
- </details>
586
482
 
587
483
  <details class="micro panel-details" id="micro-manage">
588
484
  <summary>
@@ -658,7 +554,7 @@
658
554
  the A/B compare panel); the composer itself never opens, closes,
659
555
  or relocates - except into #conversation-focus-panels during
660
556
  "expand conversation", handled by openConversationFocus/
661
- closeConversationFocus alongside #thread-panel/#coach-panel. -->
557
+ closeConversationFocus alongside #thread-panel. -->
662
558
  <div id="persistent-composer">
663
559
  <div class="composer-suggestions" id="composer-suggestions" hidden>
664
560
  <div id="cp-results" class="cp-results-scroll">
@@ -690,10 +586,89 @@
690
586
  <button type="button" id="cp-armed-job-clear" aria-label="Change job">&times;</button>
691
587
  </div>
692
588
  <div class="staged-attachments" id="staged-attachments"></div>
589
+ <div class="review-actions" id="review-actions" hidden>
590
+ <button class="rb-approve" type="button" id="review-approve">&#10003; Approve</button>
591
+ <button class="rb-secondary" type="button" id="review-request-changes">&#8617; Request changes</button>
592
+ <span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
593
+ </div>
693
594
  <div class="coach-input">
694
595
  <button class="attach-btn" type="button" id="attach-btn" aria-label="Attach a file or screenshot" title="Attach a file or screenshot">&#128206;</button>
695
596
  <input type="file" id="attach-file-input" multiple accept="image/png,image/jpeg,image/gif,.pdf,.txt,.log" hidden>
696
- <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-armed="Now type what you want done…"></textarea>
597
+ <div class="quick-coach-row composer-coach-actions" id="quick-coach-btns" hidden>
598
+ <button class="ghost quick-coach-btn mark-complete-btn composer-icon-btn" type="button" id="mark-complete-btn" aria-label="Mark the job complete" title="Mark complete">&#10003;</button>
599
+ <button class="run-stop-btn composer-stop-btn" id="run-stop-btn" type="button" hidden aria-label="Stop the employee" title="Stop the employee">&#9209;</button>
600
+ <div class="coach-dropdown" id="positive-reinforcement-wrap">
601
+ <button class="ghost quick-coach-btn composer-icon-btn" type="button" id="positive-reinforcement-btn" data-testid="positive-reinforcement-btn" aria-label="Positive reinforcement" title="Positive reinforcement" aria-haspopup="menu" aria-expanded="false" aria-controls="positive-reinforcement-dropdown">&#128077;</button>
602
+ <div class="coach-dropdown-menu" id="positive-reinforcement-dropdown" data-testid="positive-reinforcement-dropdown" hidden role="menu">
603
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
604
+ data-job="recognize-good-work"
605
+ title="Capture what went well so the employee repeats it">Well done</button>
606
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
607
+ data-job="praise-fraim"
608
+ title="Record praise for the employee in the FRAIM registry">Praise publicly</button>
609
+ </div>
610
+ </div>
611
+ <div class="coach-dropdown" id="negative-reinforcement-wrap">
612
+ <button class="ghost quick-coach-btn composer-icon-btn" type="button" id="negative-reinforcement-btn" data-testid="negative-reinforcement-btn" aria-label="Negative reinforcement" title="Negative reinforcement" aria-haspopup="menu" aria-expanded="false" aria-controls="negative-reinforcement-dropdown">&#128078;</button>
613
+ <div class="coach-dropdown-menu" id="negative-reinforcement-dropdown" data-testid="negative-reinforcement-dropdown" hidden role="menu">
614
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
615
+ data-job="analyze-why-you-messed-up"
616
+ title="Ask the employee to diagnose the root cause and what to change next time">Analyze your mistake</button>
617
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
618
+ data-job="follow-your-mentor"
619
+ title="Ask the employee to re-read the workflow instructions and continue from the correct next step">Follow due process</button>
620
+ </div>
621
+ </div>
622
+ <div class="coach-dropdown" id="conviction-wrap">
623
+ <button class="ghost quick-coach-btn composer-icon-btn" type="button" id="conviction-btn" data-testid="conviction-btn" aria-label="Your conviction" title="Your conviction" aria-haspopup="menu" aria-expanded="false" aria-controls="conviction-dropdown">&#129517;</button>
624
+ <div class="coach-dropdown-menu" id="conviction-dropdown" data-testid="conviction-dropdown" hidden role="menu">
625
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
626
+ data-job="strong-pov"
627
+ title="Ask the employee to defend a strong, well-reasoned position">Strong pov</button>
628
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
629
+ data-job="weak-pov"
630
+ title="Ask the employee to acknowledge uncertainty and present a hedged view">Weak pov</button>
631
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
632
+ data-job="need-pov"
633
+ title="Ask the employee to develop a point of view where they currently have none">Need pov</button>
634
+ </div>
635
+ </div>
636
+ <div class="coach-dropdown" id="explain-wrap">
637
+ <button class="ghost quick-coach-btn composer-icon-btn" type="button" id="explain-btn" data-testid="explain-btn" aria-label="Explain it" title="Explain it" aria-haspopup="menu" aria-expanded="false" aria-controls="explain-dropdown">?</button>
638
+ <div class="coach-dropdown-menu" id="explain-dropdown" data-testid="explain-dropdown" hidden role="menu">
639
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
640
+ data-job="explain-it"
641
+ data-explain-mode="simpler"
642
+ data-prompt="Explain this more simply."
643
+ title="Ask for a shorter explanation with less jargon">Simpler</button>
644
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
645
+ data-job="explain-it"
646
+ data-explain-mode="business"
647
+ data-prompt="Explain this in business terms."
648
+ title="Ask for impact, tradeoffs, and decision context">Business terms</button>
649
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
650
+ data-job="explain-it"
651
+ data-explain-mode="technical"
652
+ data-prompt="Explain this in technical terms."
653
+ title="Ask for implementation details and technical caveats">Technical terms</button>
654
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
655
+ data-job="explain-it"
656
+ data-explain-mode="five-year-old"
657
+ data-prompt="Explain this like I am 5."
658
+ title="Ask for a very simple explanation">Like a 5 year old</button>
659
+ <button class="ghost quick-coach-btn" type="button" role="menuitem"
660
+ data-job="explain-it"
661
+ data-explain-mode="preferred"
662
+ data-prompt="Explain this the way I like it."
663
+ title="Ask the employee to use your learned explanation preferences">The way I like it</button>
664
+ </div>
665
+ </div>
666
+ </div>
667
+ <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>
668
+ <span class="active-employee-row composer-agent-row" hidden>
669
+ <span class="active-employee-label">Maestro</span>
670
+ <select id="active-employee-select" class="employee-select inline" aria-label="Agent Tool"></select>
671
+ </span>
697
672
  <button class="send-button" type="button" id="send" aria-label="Send" title="Send" disabled>↑</button>
698
673
  </div>
699
674
  <div class="composer-hint" id="composer-hint">Attach or paste a screenshot, log, or PDF, up to 10&nbsp;MB each.</div>
@@ -39,8 +39,7 @@
39
39
  }
40
40
  .rc-art-btn--teach:hover { background: #ece3ff; }
41
41
 
42
- /* ── R7.7: unified action bar — review actions sit beside the always-on
43
- coaching chips inside the coach panel. The chips/input never disappear. ── */
42
+ /* R7.7: review actions stay next to the always-on composer controls. */
44
43
  .review-actions {
45
44
  display: flex;
46
45
  flex-wrap: wrap;
@@ -134,10 +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
- // Issue #1176: continue-run delivery control.
138
- 'delivery-control', 'delivery-after', 'delivery-redirect', 'delivery-help',
139
- 'coach-panel', 'coach-summary',
140
- 'thread-panel', 'thread-panel-kicker', 'coach-panel-kicker',
137
+ 'thread-panel', 'thread-panel-kicker',
141
138
  'quick-coach-btns',
142
139
  'positive-reinforcement-btn', 'positive-reinforcement-dropdown',
143
140
  'negative-reinforcement-btn', 'negative-reinforcement-dropdown',
@@ -2068,11 +2065,8 @@ function panelStateFor(convId) {
2068
2065
  }
2069
2066
 
2070
2067
  function defaultCoachOpen(conv) {
2071
- // Issue #1021 (R11): an oversight run collapses the coach panel by default so the
2072
- // delegation boards get the vertical space. But the review actions live inside that
2073
- // panel, so once Mandy submits, the human had to expand a collapsed panel to find
2074
- // Approve while every other employee shows it immediately. Parity is the whole point
2075
- // of this issue: when the run is actually awaiting a decision, the decision is visible.
2068
+ // Preserve the historic per-conversation default for review visibility, even though
2069
+ // the coaching controls now live in the persistent composer.
2076
2070
  if (isManagerOversightConversation(conv)) {
2077
2071
  return typeof convAwaitingReview === 'function' ? convAwaitingReview(conv) : false;
2078
2072
  }
@@ -2109,16 +2103,6 @@ function syncThreadPanelKicker(conv) {
2109
2103
  el.textContent = name ? `${name} updates` : 'Updates';
2110
2104
  }
2111
2105
 
2112
- // R3: update the coach panel kicker with "[User]'s coaching to [Employee]".
2113
- function syncCoachPanelKicker(conv) {
2114
- const el = els['coach-panel-kicker'] || document.getElementById('coach-panel-kicker');
2115
- if (!el) return;
2116
- if (!conv) { el.textContent = 'Your coaching'; return; }
2117
- const manager = managerLabel();
2118
- const employee = coachEmployeeLabel(conv);
2119
- el.textContent = `${manager}'s coaching to ${employee}`;
2120
- }
2121
-
2122
2106
  // Returns the FRAIM invocation prefix for the currently active employee:
2123
2107
  // $fraim for Codex, /fraim for all others (mirrors manager-turns.ts).
2124
2108
  function fraimPrefix() {
@@ -2132,7 +2116,8 @@ function syncConvictionDropdownLabel() {
2132
2116
  const btn = els['conviction-btn'] || document.getElementById('conviction-btn');
2133
2117
  if (!btn) return;
2134
2118
  const manager = managerLabel();
2135
- btn.textContent = `${manager}'s conviction ▾`;
2119
+ btn.title = `${manager}'s conviction`;
2120
+ btn.setAttribute('aria-label', `${manager}'s conviction`);
2136
2121
  }
2137
2122
 
2138
2123
  // R4: update coach label with employee name + initial badge.
@@ -2166,13 +2151,57 @@ function syncCoachEmployeeLabel(conv) {
2166
2151
  // R5: show/hide quick-access coaching buttons based on managerTemplates.
2167
2152
  function syncQuickCoachButtons(conv) {
2168
2153
  const row = els['quick-coach-btns'];
2169
- if (!row) return;
2170
- row.hidden = !conv;
2154
+ if (row) row.hidden = !conv;
2155
+ const agentRow = document.querySelector('.composer-agent-row');
2156
+ if (agentRow) agentRow.hidden = !conv;
2171
2157
  if (els['mark-complete-btn']) {
2172
2158
  els['mark-complete-btn'].hidden = !conv;
2173
2159
  }
2174
2160
  }
2175
2161
 
2162
+ function composerIsArmed() {
2163
+ return !!(
2164
+ (els['coach-text'] && els['coach-text'].value.trim().length > 0) ||
2165
+ state.pendingCoachingJobId ||
2166
+ hasValidStagedAttachment()
2167
+ );
2168
+ }
2169
+
2170
+ function syncComposerPlaceholder(conv = activeConversation()) {
2171
+ const textarea = els['coach-text'];
2172
+ if (!textarea || textarea.disabled) return;
2173
+ if (state.cpSelectedJob) {
2174
+ textarea.placeholder = textarea.dataset.placeholderArmed || 'Now type what you want done...';
2175
+ } else if (conv) {
2176
+ textarea.placeholder = textarea.dataset.placeholderActive || 'Coach this run, or type new instructions...';
2177
+ } else {
2178
+ textarea.placeholder = textarea.dataset.placeholderIdle || 'Type a job name, /handle, or describe what you need...';
2179
+ }
2180
+ }
2181
+
2182
+ function syncComposerStopButton(conv = activeConversation()) {
2183
+ const stopBtn = els['run-stop-btn'];
2184
+ if (!stopBtn) return;
2185
+ stopBtn.hidden = !conv;
2186
+ if (!conv) return;
2187
+ const canStop = conv.status === 'running' && !!conv.runId && !conv._stopping;
2188
+ stopBtn.disabled = !canStop;
2189
+ stopBtn.classList.toggle('composer-stop-btn--armed', canStop && composerIsArmed());
2190
+ if (conv._stopping) {
2191
+ stopBtn.textContent = '⏹ Stopping...';
2192
+ stopBtn.setAttribute('aria-label', 'Stopping the employee, please wait');
2193
+ stopBtn.title = 'Stopping the employee, please wait';
2194
+ } else if (canStop && composerIsArmed()) {
2195
+ stopBtn.textContent = '⏹ Stop & send';
2196
+ stopBtn.setAttribute('aria-label', 'Stop the employee and send the armed coaching now');
2197
+ stopBtn.title = 'Stop the employee and send the armed coaching now';
2198
+ } else {
2199
+ stopBtn.textContent = '⏹';
2200
+ stopBtn.setAttribute('aria-label', canStop ? 'Stop the employee' : 'Nothing is running to stop');
2201
+ stopBtn.title = canStop ? 'Stop the employee' : 'Nothing is running to stop';
2202
+ }
2203
+ }
2204
+
2176
2205
  function stripReviewHandoffBlocks(text) {
2177
2206
  return String(text || '')
2178
2207
  .replace(/<review_handoff>\s*[\s\S]*?\s*<\/review_handoff>/gi, '')
@@ -2274,19 +2303,13 @@ function formatEmployeeText(text) {
2274
2303
  }
2275
2304
 
2276
2305
  function syncConversationPanels(conv, switchedConv) {
2277
- const coach = els['coach-panel'];
2278
- if (!conv || !coach) return;
2306
+ if (!conv) return;
2279
2307
  const panelState = panelStateFor(conv.id);
2280
2308
  if (switchedConv) {
2281
- coach.open = panelState.coach ?? defaultCoachOpen(conv);
2282
2309
  if (els['thread-panel']) {
2283
2310
  els['thread-panel'].open = panelState.thread ?? true;
2284
2311
  }
2285
2312
  }
2286
- // R3.3: hide the summary hint when the coach panel is open.
2287
- if (els['coach-summary']) {
2288
- els['coach-summary'].hidden = coach.open;
2289
- }
2290
2313
  }
2291
2314
 
2292
2315
  function upsertConversation(conv, options) {
@@ -3633,12 +3656,9 @@ const conversationFocusState = {
3633
3656
  previousFocus: null,
3634
3657
  threadParent: null,
3635
3658
  threadNext: null,
3636
- coachParent: null,
3637
- coachNext: null,
3638
- // Issue #1657 Treatment 3: the composer moved out of #coach-panel to be
3639
- // permanently docked (persistent-composer.md pattern); "expand
3640
- // conversation" must move it into the overlay alongside thread/coach-panel
3641
- // too, or the manager loses the ability to type while expanded.
3659
+ // Issue #1657 Treatment 3: the composer is permanently docked; "expand
3660
+ // conversation" moves it into the overlay alongside the thread so the
3661
+ // manager can still type while expanded.
3642
3662
  composerParent: null,
3643
3663
  composerNext: null,
3644
3664
  messageScrollTop: 0,
@@ -3731,7 +3751,6 @@ function syncConversationFocusTitle(conv) {
3731
3751
 
3732
3752
  function restoreFocusedConversationNodes() {
3733
3753
  const thread = els['thread-panel'];
3734
- const coach = els['coach-panel'];
3735
3754
  const composer = document.getElementById('persistent-composer');
3736
3755
  if (thread && conversationFocusState.threadParent) {
3737
3756
  const next = conversationFocusState.threadNext;
@@ -3741,14 +3760,6 @@ function restoreFocusedConversationNodes() {
3741
3760
  conversationFocusState.threadParent.appendChild(thread);
3742
3761
  }
3743
3762
  }
3744
- if (coach && conversationFocusState.coachParent) {
3745
- const next = conversationFocusState.coachNext;
3746
- if (next && next.parentNode === conversationFocusState.coachParent) {
3747
- conversationFocusState.coachParent.insertBefore(coach, next);
3748
- } else {
3749
- conversationFocusState.coachParent.appendChild(coach);
3750
- }
3751
- }
3752
3763
  if (composer && conversationFocusState.composerParent) {
3753
3764
  const next = conversationFocusState.composerNext;
3754
3765
  if (next && next.parentNode === conversationFocusState.composerParent) {
@@ -3784,17 +3795,14 @@ function openConversationFocus() {
3784
3795
  const overlay = els['conversation-focus-overlay'];
3785
3796
  const panels = els['conversation-focus-panels'];
3786
3797
  const thread = els['thread-panel'];
3787
- const coach = els['coach-panel'];
3788
3798
  const composer = document.getElementById('persistent-composer');
3789
- if (!conv || !overlay || !panels || !thread || !coach) return;
3799
+ if (!conv || !overlay || !panels || !thread) return;
3790
3800
  if (conversationFocusState.open) return;
3791
3801
 
3792
3802
  conversationFocusState.open = true;
3793
3803
  conversationFocusState.previousFocus = document.activeElement;
3794
3804
  conversationFocusState.threadParent = thread.parentNode;
3795
3805
  conversationFocusState.threadNext = thread.nextSibling;
3796
- conversationFocusState.coachParent = coach.parentNode;
3797
- conversationFocusState.coachNext = coach.nextSibling;
3798
3806
  if (composer) {
3799
3807
  conversationFocusState.composerParent = composer.parentNode;
3800
3808
  conversationFocusState.composerNext = composer.nextSibling;
@@ -3803,7 +3811,6 @@ function openConversationFocus() {
3803
3811
 
3804
3812
  syncConversationFocusTitle(conv);
3805
3813
  panels.appendChild(thread);
3806
- panels.appendChild(coach);
3807
3814
  if (composer) panels.appendChild(composer);
3808
3815
  overlay.hidden = false;
3809
3816
  overlay.classList.add('open');
@@ -3840,6 +3847,9 @@ function renderActive() {
3840
3847
  if (els['ab-direct-panel']) els['ab-direct-panel'].hidden = true;
3841
3848
  // Issue #512 R7: clear any lingering review UI when no conversation is active.
3842
3849
  if (els['review-actions']) els['review-actions'].hidden = true;
3850
+ syncQuickCoachButtons(null);
3851
+ syncComposerPlaceholder(null);
3852
+ syncComposerStopButton(null);
3843
3853
  // #770: hide the Deliverables panel when there is no active conversation.
3844
3854
  if (els['deliverables-panel']) { els['deliverables-panel'].hidden = true; els['deliverables-body'].innerHTML = ''; }
3845
3855
  syncThreadUiState(null);
@@ -3867,9 +3877,9 @@ function renderActive() {
3867
3877
  renderJobModeChip(conv);
3868
3878
  syncCoachEmployeeLabel(conv);
3869
3879
  syncThreadPanelKicker(conv);
3870
- syncCoachPanelKicker(conv);
3871
3880
  syncConvictionDropdownLabel();
3872
3881
  syncCoachNote(conv);
3882
+ syncComposerPlaceholder(conv);
3873
3883
  if (conversationFocusState.open) syncConversationFocusTitle(conv);
3874
3884
 
3875
3885
  // If we switched conversations (or this is the first render), wipe and
@@ -3913,6 +3923,11 @@ function renderActive() {
3913
3923
  syncThreadUiState(conv);
3914
3924
  syncConversationPanels(conv, switchedConv);
3915
3925
  syncQuickCoachButtons(conv);
3926
+ // Issue #1723: renderActive() runs on every poll cycle for the active
3927
+ // conversation but previously never re-synced the Stop button here, so a
3928
+ // newly-started conversation showed a stale "Stop & send" (armed) label
3929
+ // until whichever other call site happened to run next.
3930
+ syncComposerStopButton(conv);
3916
3931
  syncManagedDelegationAccess(conv);
3917
3932
  syncDeliveryControl(conv);
3918
3933
  ensureThreadMessageViewportObserver();
@@ -3988,18 +4003,11 @@ function renderActive() {
3988
4003
  // genuinely new messages, not every row a messagesMutated full rebuild just
3989
4004
  // re-appended (e.g. an existing message's badge disappearing).
3990
4005
  const priorRenderedMessageCount = renderedMessageCount;
3991
- const hadPendingRowsBeforeRebuild = !!els['messages'].querySelector('.message[data-pending="true"]');
3992
4006
  // Issue #1665: capture the manager's reading position before this render touches the thread.
3993
4007
  const threadHost = els['messages'];
3994
4008
  const threadScrollBefore = threadHost.scrollTop;
3995
4009
  const threadWasNearBottom = threadHost.scrollHeight - threadScrollBefore - threadHost.clientHeight < 80;
3996
- if (
3997
- hadPendingRowsBeforeRebuild
3998
- && els['messages'].scrollTop <= 20
3999
- && els['messages'].scrollHeight - els['messages'].clientHeight > 80
4000
- ) {
4001
- userScrolledAwayDuringPendingAnchor = true;
4002
- }
4010
+ const hadPendingRowsBeforeRebuild = !!threadHost.querySelector('.message[data-pending="true"]');
4003
4011
  const rebuildThread = messages.length < renderedMessageCount || messagesMutated;
4004
4012
  if (rebuildThread) {
4005
4013
  els['messages'].innerHTML = '';
@@ -4025,10 +4033,10 @@ function renderActive() {
4025
4033
  // scrolling, even if they had scrolled up to review earlier history.
4026
4034
  const hasNewPendingDeliveryMessage = messages.slice(priorRenderedMessageCount).some((message) => !!message.deliveryStatus);
4027
4035
  const hasPendingDeliveryMessage = messages.some((message) => !!message.deliveryStatus);
4036
+ const clearedPendingDeliveryMessages = hadPendingRowsBeforeRebuild && !hasPendingDeliveryMessage;
4028
4037
  // Issue #1309/#1570: when a poll clears the queued/redirecting backlog, the
4029
4038
  // thread should reconcile to the live bottom with the delivered turn rather
4030
4039
  // than staying parked at the old pending-anchor position.
4031
- const clearedPendingDeliveryMessages = hadPendingRowsBeforeRebuild && !hasPendingDeliveryMessage;
4032
4040
  syncWorkingIndicator(conv);
4033
4041
  // Direct raw event log — append-only like the FRAIM micro-log, shows all
4034
4042
  // stdout/stderr/system events from the B run so the user sees everything.
@@ -4072,18 +4080,18 @@ function renderActive() {
4072
4080
  if (typeof convFindRefresh === 'function') convFindRefresh();
4073
4081
  const shouldScrollForUpdate = switchedConv || statusChanged || appendedMessages > 0 || messagesMutated;
4074
4082
  const reviewFollow = switchedConv || statusChanged || threadWasNearBottom;
4075
- scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage || clearedPendingDeliveryMessages, reviewFollow);
4083
+ scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
4076
4084
  requestAnimationFrame(() => {
4077
4085
  syncThreadMessageViewport();
4078
- scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage || clearedPendingDeliveryMessages, reviewFollow);
4086
+ scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
4079
4087
  });
4080
4088
  setTimeout(() => {
4081
4089
  syncThreadMessageViewport();
4082
- scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage || clearedPendingDeliveryMessages, reviewFollow);
4090
+ scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
4083
4091
  });
4084
4092
  setTimeout(() => {
4085
4093
  syncThreadMessageViewport();
4086
- scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage || clearedPendingDeliveryMessages, reviewFollow);
4094
+ scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
4087
4095
  }, 50);
4088
4096
 
4089
4097
  // Issue #347 — tracker, totals, picker. The data lives on conv.run
@@ -4242,23 +4250,7 @@ function renderRunStatePill(conv) {
4242
4250
  // conversationStateLabel returns 'Stopped' for that state; toUpperCase() => 'STOPPED'.
4243
4251
  pill.textContent = conversationStateLabel(conv).toUpperCase();
4244
4252
  pill.className = `run-state-pill ${conversationUiState(conv)}`;
4245
- // #521: Stop is offered only while the employee is actively working.
4246
- const stopBtn = els['run-stop-btn'];
4247
- if (stopBtn) {
4248
- const canStop = conv.status === 'running' && !!conv.runId && !conv._stopping;
4249
- // #549 R1/AC1.3: While in-flight (_stopping===true), keep the button VISIBLE but disabled
4250
- // so the manager retains the affordance showing "Stopping…". Hide only when the run is
4251
- // not active at all (canStop===false AND not in-flight).
4252
- stopBtn.hidden = !canStop && !conv._stopping;
4253
- stopBtn.disabled = !!conv._stopping;
4254
- if (conv._stopping) {
4255
- stopBtn.textContent = '⏹ Stopping…';
4256
- stopBtn.setAttribute('aria-label', 'Stopping the employee, please wait');
4257
- } else {
4258
- stopBtn.textContent = '⏹ Stop';
4259
- stopBtn.setAttribute('aria-label', 'Ask the employee to stop and wait for you');
4260
- }
4261
- }
4253
+ syncComposerStopButton(conv);
4262
4254
  }
4263
4255
 
4264
4256
  function renderResumeCommand(conv) {
@@ -5373,9 +5365,32 @@ function positionCoachDropdown(dropdown, btn) {
5373
5365
  const opensBelow = availableBelow >= Math.min(menuHeight, 160) || availableBelow >= availableAbove;
5374
5366
  const availableSpace = opensBelow ? availableBelow : availableAbove;
5375
5367
  const renderedHeight = Math.min(menuHeight, availableSpace || menuHeight);
5376
- const top = opensBelow
5377
- ? Math.min(rect.bottom + margin, window.innerHeight - renderedHeight - viewportPad)
5378
- : Math.max(viewportPad, rect.top - renderedHeight - margin);
5368
+ let top;
5369
+ if (opensBelow) {
5370
+ top = Math.min(rect.bottom + margin, window.innerHeight - renderedHeight - viewportPad);
5371
+ } else {
5372
+ top = Math.max(viewportPad, rect.top - renderedHeight - margin);
5373
+ }
5374
+ // Issue #1723: the composer textarea (#coach-text) shares the same
5375
+ // toolbar box as every coach-dropdown trigger, and is taller than the
5376
+ // trigger row - it extends both above and below the trigger's own edges.
5377
+ // A placement chosen purely from the trigger's own rect (either branch
5378
+ // above) can still land on top of the textarea. If it does, push the
5379
+ // menu to whichever side of the textarea actually clears it and has
5380
+ // room; otherwise leave the original placement (no collision-free
5381
+ // option fits in the viewport).
5382
+ const composerTextarea = document.getElementById('coach-text');
5383
+ const textareaRect = composerTextarea ? composerTextarea.getBoundingClientRect() : null;
5384
+ const overlapsTextarea = textareaRect && top < textareaRect.bottom && top + renderedHeight > textareaRect.top;
5385
+ if (overlapsTextarea) {
5386
+ const spaceBelowTextarea = window.innerHeight - textareaRect.bottom - margin - viewportPad;
5387
+ const spaceAboveTextarea = textareaRect.top - margin - viewportPad;
5388
+ if (spaceBelowTextarea >= renderedHeight) {
5389
+ top = textareaRect.bottom + margin;
5390
+ } else if (spaceAboveTextarea >= renderedHeight) {
5391
+ top = Math.max(viewportPad, textareaRect.top - renderedHeight - margin);
5392
+ }
5393
+ }
5379
5394
  dropdown.style.left = Math.round(left) + 'px';
5380
5395
  dropdown.style.top = Math.round(top) + 'px';
5381
5396
  dropdown.style.maxHeight = Math.max(80, Math.floor(availableSpace || (window.innerHeight - viewportPad * 2))) + 'px';
@@ -5710,13 +5725,6 @@ function ensurePendingAnchorScrollListener(host) {
5710
5725
  window.addEventListener('wheel', markUserScroll, { passive: true, capture: true });
5711
5726
  window.addEventListener('touchmove', markUserScroll, { passive: true, capture: true });
5712
5727
  host.addEventListener('mousedown', (event) => { if (event.target === host) markUserScroll(); });
5713
- host.addEventListener('scroll', () => {
5714
- if (pendingAnchorShownAt === null) return;
5715
- if (Date.now() <= pendingAnchorProgrammaticScrollUntil) return;
5716
- const pendingRow = host.querySelector('.message[data-pending="true"]');
5717
- if (!pendingRow) return;
5718
- if (host.scrollTop <= 20 && host.scrollHeight - host.clientHeight > 80) markUserScroll();
5719
- }, { passive: true });
5720
5728
  }
5721
5729
 
5722
5730
  function setThreadScrollTop(host, top) {
@@ -5728,7 +5736,7 @@ function pendingAnchorUserScrolled(host) {
5728
5736
  return userScrolledAwayDuringPendingAnchor || host.dataset.pendingAnchorUserScroll === 'true' || window.__fraimPendingAnchorUserScroll === true;
5729
5737
  }
5730
5738
 
5731
- function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom, reviewFollow = true) {
5739
+ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom, reviewFollow = true, releaseBottom = false) {
5732
5740
  const latest = activeConversation();
5733
5741
  if (!latest || latest.id !== conv.id) return;
5734
5742
  const host = els['messages'];
@@ -5772,36 +5780,19 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom,
5772
5780
  }
5773
5781
  const anchorGraceActive = !!pendingRow && pendingAnchorShownAt !== null &&
5774
5782
  (Date.now() - pendingAnchorShownAt) < PENDING_ANCHOR_GRACE_MS;
5775
- if (
5776
- pendingRow
5777
- && pendingAnchorShownAt !== null
5778
- && !forceBottom
5779
- && Date.now() > pendingAnchorProgrammaticScrollUntil
5780
- && host.scrollTop <= 20
5781
- && host.scrollHeight - host.clientHeight > 80
5782
- ) {
5783
- userScrolledAwayDuringPendingAnchor = true;
5784
- host.dataset.pendingAnchorUserScroll = 'true';
5785
- window.__fraimPendingAnchorUserScroll = true;
5786
- }
5787
5783
  // Fires exactly once, on the tick where the anchor stops being active —
5788
5784
  // either the pending badge just cleared, or the grace window for a
5789
5785
  // still-unresolved pending row (e.g. Defect 1's kill still in flight)
5790
5786
  // just elapsed. Either way the manager has "seen" the anchor; resume
5791
5787
  // following live content on this tick instead of staying wherever the
5792
5788
  // anchor last parked the view.
5793
- const catchUpToBottom = pendingAnchorGraceActiveLastTick && !anchorGraceActive;
5789
+ const catchUpToBottom = releaseBottom || (pendingAnchorGraceActiveLastTick && !anchorGraceActive);
5794
5790
  pendingAnchorGraceActiveLastTick = anchorGraceActive;
5795
5791
 
5796
5792
  if (!shouldScrollForUpdate && !forceBottom && !catchUpToBottom) return;
5797
5793
  // #936: re-evaluate nearBottom at call time so deferred invocations respect
5798
5794
  // any scroll the user made between the render tick and this callback.
5799
5795
  const nearBottom = host.scrollHeight - host.scrollTop - host.clientHeight < 80;
5800
- if (catchUpToBottom && host.scrollTop <= 20 && host.scrollHeight - host.clientHeight > 80) {
5801
- userScrolledAwayDuringPendingAnchor = true;
5802
- host.dataset.pendingAnchorUserScroll = 'true';
5803
- window.__fraimPendingAnchorUserScroll = true;
5804
- }
5805
5796
  if (catchUpToBottom && pendingAnchorUserScrolled(host)) return;
5806
5797
  if (forceBottom && pendingAnchorUserScrolled(host)) return;
5807
5798
  if (anchorGraceActive && !pendingAnchorUserScrolled(host)) {
@@ -5812,7 +5803,13 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom,
5812
5803
  // mid-turn (queued or redirecting) must be visible without the manager
5813
5804
  // having to scroll, even if they had scrolled up to review history.
5814
5805
  if (nearBottom || forceBottom || catchUpToBottom) {
5815
- setThreadScrollTop(host, anchorGraceActive ? pendingBacklogScrollTop(host) : host.scrollHeight);
5806
+ const toPendingAnchor = anchorGraceActive;
5807
+ setThreadScrollTop(host, toPendingAnchor ? pendingBacklogScrollTop(host) : host.scrollHeight);
5808
+ if (!toPendingAnchor) {
5809
+ setTimeout(() => {
5810
+ if (!pendingAnchorUserScrolled(host)) setThreadScrollTop(host, host.scrollHeight);
5811
+ }, 320);
5812
+ }
5816
5813
  }
5817
5814
  return;
5818
5815
  } else if (shouldScrollForUpdate && reviewFollow) {
@@ -6092,6 +6089,7 @@ function syncSendButton() {
6092
6089
  const hasText = els['coach-text'].value.trim().length > 0;
6093
6090
  if (isManagedDelegationChild(conv)) {
6094
6091
  els['send'].disabled = true;
6092
+ syncComposerStopButton(conv);
6095
6093
  return;
6096
6094
  }
6097
6095
  // Issue #1651 R6: a valid staged attachment is enough to enable Send even
@@ -6102,6 +6100,7 @@ function syncSendButton() {
6102
6100
  // resume, it just needs something to send; startRun creates the session.
6103
6101
  if (!conv) {
6104
6102
  els['send'].disabled = !(hasText || hasAttachment);
6103
+ syncComposerStopButton(conv);
6105
6104
  return;
6106
6105
  }
6107
6106
  // Send is enabled as soon as the host session exists. We deliberately
@@ -6117,6 +6116,7 @@ function syncSendButton() {
6117
6116
  // wants to fire that coaching directive, optionally with added context.
6118
6117
  const hasPendingJob = !!state.pendingCoachingJobId;
6119
6118
  els['send'].disabled = !((hasText || hasPendingJob || hasAttachment) && (resumable || canRestartWithCoaching));
6119
+ syncComposerStopButton(conv);
6120
6120
  }
6121
6121
 
6122
6122
  function defaultCoachNote(conv) {
@@ -6161,27 +6161,8 @@ function syncCoachNote(conv = activeConversation()) {
6161
6161
  els['coach-note'].textContent = defaultCoachNote(conv);
6162
6162
  }
6163
6163
 
6164
- // Issue #1176 R34/R35/R36/R42: show/hide and label the "After current" /
6165
- // "Redirect now" delivery control. Visible only while the run is actively
6166
- // WORKING (conversationUiState reuses the same classification the pill and
6167
- // working-indicator already trust), independent of textarea focus/typing.
6168
- const DELIVERY_HELP_TEXT = {
6169
- after: 'Adds this message to the session queue.',
6170
- stop: 'Stops the current turn, then sends this correction in the same session.',
6171
- };
6172
6164
  function syncDeliveryControl(conv = activeConversation()) {
6173
- const control = els['delivery-control'];
6174
- const help = els['delivery-help'];
6175
- if (!control || !help) return;
6176
- const visible = !!conv && conversationUiState(conv) === 'working' && !isManagedDelegationChild(conv);
6177
- control.hidden = !visible;
6178
- help.hidden = !visible;
6179
- if (!visible) return;
6180
- const isRedirect = state.deliveryIntent === 'stop';
6181
- if (els['delivery-after']) els['delivery-after'].setAttribute('aria-pressed', String(!isRedirect));
6182
- if (els['delivery-redirect']) els['delivery-redirect'].setAttribute('aria-pressed', String(isRedirect));
6183
- help.textContent = isRedirect ? DELIVERY_HELP_TEXT.stop : DELIVERY_HELP_TEXT.after;
6184
- help.classList.toggle('redirect', isRedirect);
6165
+ syncComposerStopButton(conv);
6185
6166
  }
6186
6167
 
6187
6168
  function selectDeliveryIntent(intent) {
@@ -6199,7 +6180,6 @@ function syncManagedDelegationAccess(conv = activeConversation()) {
6199
6180
  : 'Tell the employee what to do next...';
6200
6181
  }
6201
6182
  if (els['send']) els['send'].disabled = managed || els['send'].disabled;
6202
- if (els['coach-panel']) els['coach-panel'].classList.toggle('coach-panel--managed-readonly', managed);
6203
6183
  for (const selector of ['#quick-coach-btns button', '#mark-complete-btn', '#positive-reinforcement-btn', '#negative-reinforcement-btn', '#conviction-btn', '#explain-btn', '#review-actions button']) {
6204
6184
  for (const button of document.querySelectorAll(selector)) {
6205
6185
  if (managed) {
@@ -6215,6 +6195,8 @@ function syncManagedDelegationAccess(conv = activeConversation()) {
6215
6195
  }
6216
6196
  }
6217
6197
  if (managed) closeAllCoachDropdowns();
6198
+ syncComposerPlaceholder(conv);
6199
+ syncComposerStopButton(conv);
6218
6200
  }
6219
6201
 
6220
6202
  function appendEditableCoachingPrompt(promptText) {
@@ -6294,7 +6276,7 @@ function convAwaitingReview(conv) {
6294
6276
  // via an explicit review_handoff — Mandy's delegation artifacts are orchestration working
6295
6277
  // output, not deliverables submitted to the human manager. Other manager jobs (e.g.,
6296
6278
  // operational-reporting) may legitimately surface artifacts for human review.
6297
- if (delegationLedgerForConversation(conv)) return false;
6279
+ if (isManagerOversightConversation(conv) || delegationLedgerForConversation(conv)) return false;
6298
6280
  const runArtifacts = Array.isArray(conv.artifacts) ? conv.artifacts : [];
6299
6281
  if (conv.status === 'completed' && runArtifacts.some((artifact) => artifactLocalPath(artifact) || artifactExternalUrl(artifact))) {
6300
6282
  return true;
@@ -8010,15 +7992,23 @@ async function sendCoachingMessage() {
8010
7992
  }
8011
7993
  }
8012
7994
 
7995
+ async function stopFromComposer() {
7996
+ const conv = activeConversation();
7997
+ if (!conv || conv.status !== 'running' || !conv.runId || conv._stopping) return;
7998
+ if (composerIsArmed()) {
7999
+ state.deliveryIntent = 'stop';
8000
+ await sendCoachingMessage();
8001
+ } else {
8002
+ await tfStopRun(conv);
8003
+ }
8004
+ }
8005
+
8013
8006
  function renderCpRows(searchText) {
8014
8007
  const { handle, query } = parseCpSearch(searchText);
8015
8008
  const jobs = (state.bootstrap && state.bootstrap.jobs) ? state.bootstrap.jobs : [];
8016
8009
  const recentIds = (state.bootstrap && state.bootstrap.preferences && Array.isArray(state.bootstrap.preferences.recentJobIds))
8017
8010
  ? state.bootstrap.preferences.recentJobIds
8018
8011
  : [];
8019
- const recentInstructions = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.recentJobInstructions)
8020
- ? state.bootstrap.preferences.recentJobInstructions
8021
- : {};
8022
8012
  // Resolve persona filter from slash handle
8023
8013
  let personaFilter = null;
8024
8014
  if (handle !== null && handle !== '') {
@@ -8064,7 +8054,7 @@ function renderCpRows(searchText) {
8064
8054
  for (const jobId of recentIds) {
8065
8055
  const job = jobs.find((j) => j.id === jobId);
8066
8056
  if (job) {
8067
- recentRows.push({ type: 'recent', job, instructions: recentInstructions[jobId] || '' });
8057
+ recentRows.push({ type: 'recent', job, instructions: '' });
8068
8058
  if (recentRows.length >= 3) break;
8069
8059
  }
8070
8060
  }
@@ -8203,12 +8193,7 @@ function buildCpRow(row, flatIndex) {
8203
8193
  title.textContent = row.job.title || row.job.id;
8204
8194
  body.appendChild(title);
8205
8195
 
8206
- if (row.type === 'recent' && row.instructions) {
8207
- const sub = document.createElement('span');
8208
- sub.className = 'cp-row-sub';
8209
- sub.textContent = row.instructions.slice(0, 80);
8210
- body.appendChild(sub);
8211
- } else if (row.job.intent) {
8196
+ if (row.job.intent) {
8212
8197
  const sub = document.createElement('span');
8213
8198
  sub.className = 'cp-row-sub';
8214
8199
  sub.textContent = row.job.intent;
@@ -8262,13 +8247,11 @@ function renderCpHighlight() {
8262
8247
  // for a real catalog job, since a full instruction is essentially never a
8263
8248
  // substring of a job's own brief title/intent. In practice the ONLY way to
8264
8249
  // reach a catalog row is to type something short and job-name-like, which is
8265
- // exactly what broke. Picking a fresh catalog job now arms it
8250
+ // exactly what broke. Picking a catalog or recent job now arms it
8266
8251
  // (armSelectedJob) and clears the box for the manager to type real
8267
- // instructions, instead of sending immediately. Recent rows (a "run it
8268
- // again" shortcut with already-known-good instructions) and ad-hoc (no
8269
- // separate name/instructions split - the typed text already IS the
8270
- // instructions) are unaffected and still fire immediately. Teach is out of
8271
- // this catalog entirely (see #teach-btn/#teach-job-modal).
8252
+ // instructions, instead of sending immediately. Ad-hoc remains immediate-fire:
8253
+ // there is no separate name/instructions split - the typed text already IS the
8254
+ // instructions. Teach is out of this catalog entirely (see #teach-btn/#teach-job-modal).
8272
8255
  function selectCpRow(index) {
8273
8256
  const row = state.cpRows[index];
8274
8257
  if (!row) return;
@@ -8278,12 +8261,6 @@ function selectCpRow(index) {
8278
8261
  submitComposerDelegate({ id: '__freeform__', title: 'Ad-hoc task', intent: '' }, searchText);
8279
8262
  return;
8280
8263
  }
8281
- if (row.type === 'recent') {
8282
- // A recent row is a "run it again" shortcut with already-known-good
8283
- // instructions - immediate-fire, unlike a fresh catalog pick below.
8284
- submitComposerDelegate(row.job, row.instructions || searchText);
8285
- return;
8286
- }
8287
8264
  armSelectedJob(row.job);
8288
8265
  }
8289
8266
 
@@ -8326,10 +8303,10 @@ function renderArmedJobChip() {
8326
8303
  chip.hidden = false;
8327
8304
  const label = chip.querySelector('.cp-armed-job-label');
8328
8305
  if (label) label.textContent = state.cpSelectedJob.title || state.cpSelectedJob.id;
8329
- if (textEl) textEl.placeholder = textEl.dataset.placeholderArmed || 'Now type what you want done…';
8306
+ syncComposerPlaceholder();
8330
8307
  } else {
8331
8308
  chip.hidden = true;
8332
- if (textEl) textEl.placeholder = textEl.dataset.placeholderIdle || textEl.placeholder;
8309
+ syncComposerPlaceholder();
8333
8310
  }
8334
8311
  }
8335
8312
 
@@ -8858,9 +8835,7 @@ async function startRun(job, instructions, employeeId, preassignedConvId, invoke
8858
8835
  if (state.bootstrap && state.bootstrap.preferences) {
8859
8836
  const prefs = state.bootstrap.preferences;
8860
8837
  const nextIds = [job.id, ...(prefs.recentJobIds || []).filter((id) => id !== job.id)].slice(0, 8);
8861
- const nextInstr = { ...(prefs.recentJobInstructions || {}) };
8862
- if (instructions) nextInstr[job.id] = instructions.slice(0, 500);
8863
- state.bootstrap.preferences = { ...prefs, recentJobIds: nextIds, recentJobInstructions: nextInstr };
8838
+ state.bootstrap.preferences = { ...prefs, recentJobIds: nextIds };
8864
8839
  }
8865
8840
  return { ok: true, conversation: conv, run };
8866
8841
  } catch (error) {
@@ -9339,7 +9314,7 @@ function switchToConversation(id) {
9339
9314
  // query immediately after this call, which is what lands the manager on the passage (R45).
9340
9315
  if (typeof convFindClose === 'function') convFindClose();
9341
9316
  // For Company/Manager: move the shared .page into this area's conv-host and
9342
- // switch from the info view to the full coaching panel.
9317
+ // switch from the info view to the full conversation surface.
9343
9318
  if (targetArea === 'company' || targetArea === 'manager') {
9344
9319
  tfEnsurePageInArea(targetArea);
9345
9320
  const host = document.getElementById(targetArea + '-conv-host');
@@ -10172,21 +10147,7 @@ function wireEvents() {
10172
10147
  });
10173
10148
  if (els['run-stop-btn']) {
10174
10149
  els['run-stop-btn'].addEventListener('click', () => {
10175
- const conv = activeConversation();
10176
- if (conv) tfStopRun(conv);
10177
- });
10178
- }
10179
- // Issue #1176 R41: the two delivery options are plain buttons (Tab reaches
10180
- // both natively); ArrowLeft/ArrowRight/Up/Down also cycles between them.
10181
- if (els['delivery-after']) els['delivery-after'].addEventListener('click', () => selectDeliveryIntent('after'));
10182
- if (els['delivery-redirect']) els['delivery-redirect'].addEventListener('click', () => selectDeliveryIntent('stop'));
10183
- if (els['delivery-control']) {
10184
- els['delivery-control'].addEventListener('keydown', (e) => {
10185
- if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
10186
- e.preventDefault();
10187
- const next = state.deliveryIntent === 'stop' ? 'after' : 'stop';
10188
- selectDeliveryIntent(next);
10189
- (next === 'stop' ? els['delivery-redirect'] : els['delivery-after'])?.focus();
10150
+ stopFromComposer();
10190
10151
  });
10191
10152
  }
10192
10153
  els['send'].addEventListener('click', () => sendFromComposer());
@@ -10313,16 +10274,6 @@ function wireEvents() {
10313
10274
  });
10314
10275
  }
10315
10276
 
10316
- if (els['coach-panel']) {
10317
- els['coach-panel'].addEventListener('toggle', () => {
10318
- const conv = activeConversation();
10319
- if (!conv) return;
10320
- panelStateFor(conv.id).coach = els['coach-panel'].open;
10321
- // R3.3: keep coach-summary visibility in sync when toggled directly.
10322
- if (els['coach-summary']) els['coach-summary'].hidden = els['coach-panel'].open;
10323
- });
10324
- }
10325
-
10326
10277
  // R6: thread panel toggle — persist open/close state per conversation.
10327
10278
  if (els['thread-panel']) {
10328
10279
  els['thread-panel'].addEventListener('toggle', () => {
@@ -1469,12 +1469,10 @@ img.conv-employee-avatar {
1469
1469
  border-color: var(--accent);
1470
1470
  color: var(--accent-strong);
1471
1471
  }
1472
- .coach-panel--managed-readonly {
1473
- border-color: var(--line);
1474
- }
1475
- .coach-panel--managed-readonly textarea {
1472
+ .coach textarea:disabled {
1476
1473
  background: var(--soft);
1477
1474
  color: var(--muted);
1475
+ cursor: not-allowed;
1478
1476
  }
1479
1477
  .managed-disabled {
1480
1478
  opacity: 0.55;
@@ -1586,13 +1584,12 @@ img.conv-employee-avatar {
1586
1584
  margin-bottom: 8px;
1587
1585
  }
1588
1586
 
1589
- /* Employee row now lives in the coach summary (inline with kicker).
1590
- .coach-title-row no longer exists in HTML kept only for browser cache safety. */
1587
+ /* Agent picker sits in the composer control row; .coach-title-row no longer
1588
+ exists in HTML - kept only for browser cache safety. */
1591
1589
  .active-employee-row {
1592
1590
  display: flex;
1593
1591
  align-items: center;
1594
1592
  gap: 6px;
1595
- margin-left: auto;
1596
1593
  flex-shrink: 0;
1597
1594
  }
1598
1595
  .active-employee-label {
@@ -1604,7 +1601,7 @@ img.conv-employee-avatar {
1604
1601
  color: var(--text);
1605
1602
  cursor: default;
1606
1603
  }
1607
- /* Hide the name text span only badge + dropdown shown in the summary */
1604
+ /* Hide the name text span - only badge + dropdown shown in compact controls. */
1608
1605
  .coach-label-name { display: none; }
1609
1606
  .coach-employee-badge {
1610
1607
  display: inline-flex;
@@ -1619,14 +1616,11 @@ img.conv-employee-avatar {
1619
1616
  flex-shrink: 0;
1620
1617
  }
1621
1618
  img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
1622
- /* R3.3: hide coach-summary hint when the panel is open. */
1623
- #coach-panel[open] #coach-summary { display: none; }
1624
- /* R5: quick-access coaching buttons row. */
1619
+ /* Quick-access coaching buttons row. */
1625
1620
  .quick-coach-row {
1626
1621
  display: flex;
1627
1622
  flex-wrap: wrap;
1628
1623
  gap: 6px;
1629
- margin-bottom: 8px;
1630
1624
  }
1631
1625
  .quick-coach-btn {
1632
1626
  font-size: 12px;
@@ -2011,12 +2005,12 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
2011
2005
  }
2012
2006
  .coach-input textarea {
2013
2007
  width: 100%;
2014
- min-height: 52px;
2008
+ min-height: 92px;
2015
2009
  max-height: 22vh;
2016
2010
  resize: vertical;
2017
2011
  border: 1px solid var(--line);
2018
2012
  border-radius: 8px;
2019
- padding: 12px 52px 12px 44px;
2013
+ padding: 12px 170px 48px 44px;
2020
2014
  font: inherit;
2021
2015
  color: var(--text);
2022
2016
  background: var(--surface);
@@ -2063,6 +2057,75 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
2063
2057
  .attach-btn:hover { background: var(--soft); color: var(--text); }
2064
2058
  .attach-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2065
2059
 
2060
+ .composer-coach-actions {
2061
+ position: absolute;
2062
+ left: 46px;
2063
+ right: 170px;
2064
+ bottom: 7px;
2065
+ align-items: center;
2066
+ flex-wrap: nowrap;
2067
+ overflow-x: auto;
2068
+ overflow-y: visible;
2069
+ scrollbar-width: thin;
2070
+ z-index: 2;
2071
+ }
2072
+ .composer-icon-btn,
2073
+ .composer-stop-btn {
2074
+ width: 32px;
2075
+ height: 32px;
2076
+ min-width: 32px;
2077
+ padding: 0;
2078
+ border-radius: 6px;
2079
+ display: inline-flex;
2080
+ align-items: center;
2081
+ justify-content: center;
2082
+ line-height: 1;
2083
+ font-size: 14px;
2084
+ }
2085
+ .composer-icon-btn {
2086
+ border: 1px solid var(--line);
2087
+ background: var(--surface);
2088
+ color: var(--muted);
2089
+ }
2090
+ .composer-icon-btn:hover {
2091
+ background: var(--soft);
2092
+ color: var(--text);
2093
+ }
2094
+ .composer-icon-btn:focus-visible,
2095
+ .composer-stop-btn:focus-visible,
2096
+ .employee-select.inline:focus-visible {
2097
+ outline: 2px solid var(--accent);
2098
+ outline-offset: 2px;
2099
+ }
2100
+ .composer-stop-btn {
2101
+ border-color: #fda29b;
2102
+ background: #fef3f2;
2103
+ color: #b42318;
2104
+ white-space: nowrap;
2105
+ }
2106
+ .composer-stop-btn.composer-stop-btn--armed {
2107
+ width: auto;
2108
+ min-width: 112px;
2109
+ padding: 0 10px;
2110
+ }
2111
+ .composer-stop-btn:disabled {
2112
+ border-color: var(--line);
2113
+ background: var(--soft);
2114
+ color: var(--muted);
2115
+ opacity: 1;
2116
+ }
2117
+ .composer-agent-row {
2118
+ position: absolute;
2119
+ right: 47px;
2120
+ bottom: 7px;
2121
+ z-index: 2;
2122
+ }
2123
+ .composer-agent-row .employee-select.inline {
2124
+ height: 32px;
2125
+ max-width: 116px;
2126
+ background: var(--surface);
2127
+ }
2128
+
2066
2129
  /* Issue #1657 (Treatment 1): the task-pane launcher reuses .coach-input/
2067
2130
  .attach-btn for visual consistency (R1) but keeps its own existing Next
2068
2131
  button outside the textarea wrapper (unlike the coaching composer's
@@ -2206,57 +2269,6 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
2206
2269
  color: var(--text);
2207
2270
  font-weight: 600;
2208
2271
  }
2209
- /* Issue #1176: continue-run delivery control (After current / Redirect now). */
2210
- .delivery-control {
2211
- display: inline-grid;
2212
- grid-template-columns: 1fr 1fr;
2213
- gap: 2px;
2214
- padding: 3px;
2215
- margin-bottom: 8px;
2216
- border: 1px solid var(--line);
2217
- border-radius: 7px;
2218
- background: var(--bg);
2219
- width: fit-content;
2220
- }
2221
- .delivery-option {
2222
- min-height: 28px;
2223
- padding: 5px 12px;
2224
- border: 0;
2225
- border-radius: 5px;
2226
- background: transparent;
2227
- color: var(--muted);
2228
- font-size: 11px;
2229
- font-weight: 700;
2230
- cursor: pointer;
2231
- white-space: nowrap;
2232
- }
2233
- .delivery-option:focus-visible {
2234
- outline: 2px solid var(--accent);
2235
- outline-offset: 2px;
2236
- }
2237
- .delivery-option[aria-pressed="true"] {
2238
- background: var(--surface);
2239
- color: var(--text);
2240
- box-shadow: var(--shadow);
2241
- }
2242
- /* Matches the existing .run-stop-btn danger-action colors exactly (not the
2243
- softer --danger token used for passive badges/pills below) — measured
2244
- contrast ~6:1, where --danger text on a 12% color-mix background measured
2245
- only ~3.5:1 in light mode, failing WCAG AA for this button's 11px text. */
2246
- .delivery-option--redirect[aria-pressed="true"] {
2247
- color: #b42318;
2248
- background: #fef3f2;
2249
- }
2250
- .delivery-help {
2251
- min-height: 15px;
2252
- margin: 0 1px 7px;
2253
- color: var(--muted);
2254
- font-size: 11px;
2255
- }
2256
- .delivery-help.redirect {
2257
- color: var(--danger);
2258
- font-weight: 600;
2259
- }
2260
2272
  /* Per-message queued/redirecting badge shown next to a manager bubble's meta row. */
2261
2273
  .message-delivery-badge {
2262
2274
  display: inline-flex;
@@ -2272,7 +2284,23 @@ img.coach-employee-avatar { object-fit: cover; border-radius: 4px; }
2272
2284
  color: var(--danger);
2273
2285
  }
2274
2286
  @media (max-width: 760px) {
2275
- .delivery-control { width: 100%; }
2287
+ .coach-input textarea {
2288
+ min-height: 126px;
2289
+ padding-right: 52px;
2290
+ padding-bottom: 82px;
2291
+ }
2292
+ .composer-coach-actions {
2293
+ right: 7px;
2294
+ bottom: 45px;
2295
+ }
2296
+ .composer-agent-row {
2297
+ left: 46px;
2298
+ right: 47px;
2299
+ }
2300
+ .composer-agent-row .employee-select.inline {
2301
+ width: 100%;
2302
+ max-width: none;
2303
+ }
2276
2304
  }
2277
2305
  .send-button {
2278
2306
  background: var(--accent);
@@ -4292,20 +4320,34 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4292
4320
  min-height: 0;
4293
4321
  flex: 0 1 auto;
4294
4322
  }
4295
- .workspace-conv .support-stack > .panel-details--coach { flex: 0 0 auto; }
4296
- .workspace-conv .support-stack > .panel-details--coach[open] > .panel-body {
4297
- max-height: clamp(140px, 24vh, 260px);
4298
- overflow-y: auto;
4299
- }
4300
- .workspace-conv .panel-details--coach .panel-body { padding-bottom: 8px; }
4301
4323
  .workspace-conv .coach-input textarea {
4302
- min-height: 44px;
4324
+ min-height: 92px;
4303
4325
  padding-top: 9px;
4304
- padding-bottom: 9px;
4326
+ padding-right: 170px;
4327
+ padding-bottom: 48px;
4305
4328
  }
4306
4329
  .workspace-conv .coach-note { display: none; }
4307
- .workspace-conv .quick-coach-row { gap: 5px; margin-bottom: 8px; }
4330
+ .workspace-conv .quick-coach-row { gap: 5px; }
4308
4331
  .workspace-conv .quick-coach-btn { padding: 4px 9px; }
4332
+ .workspace-conv .composer-icon-btn,
4333
+ .workspace-conv .composer-stop-btn {
4334
+ width: 32px;
4335
+ height: 32px;
4336
+ min-width: 32px;
4337
+ padding: 0;
4338
+ }
4339
+ .workspace-conv .composer-stop-btn.composer-stop-btn--armed {
4340
+ width: auto;
4341
+ min-width: 112px;
4342
+ padding: 0 10px;
4343
+ }
4344
+ @media (max-width: 760px) {
4345
+ .workspace-conv .coach-input textarea {
4346
+ min-height: 126px;
4347
+ padding-right: 52px;
4348
+ padding-bottom: 82px;
4349
+ }
4350
+ }
4309
4351
 
4310
4352
  .workspace-conv .support-stack > .micro { flex: 0 0 auto; min-height: 0; }
4311
4353
  .workspace-conv .support-stack > .micro[open] > .micro-log {
@@ -4333,11 +4375,6 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4333
4375
  max-height: 68px;
4334
4376
  overflow: hidden;
4335
4377
  }
4336
- .workspace-conv #active-conv.has-delegation-board #coach-panel[open] .panel-body,
4337
- .workspace-conv #active-conv.has-manager-oversight #coach-panel[open] .panel-body {
4338
- max-height: clamp(120px, 18vh, 240px);
4339
- overflow-y: auto;
4340
- }
4341
4378
  .workspace-conv #active-conv.has-delegation-board .support-stack,
4342
4379
  .workspace-conv #active-conv.has-manager-oversight .support-stack {
4343
4380
  flex: 0 0 auto;
@@ -4427,8 +4464,7 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4427
4464
  gap: 8px;
4428
4465
  overflow: hidden;
4429
4466
  }
4430
- .conversation-focus-panels #thread-panel,
4431
- .conversation-focus-panels #coach-panel {
4467
+ .conversation-focus-panels #thread-panel {
4432
4468
  margin: 0;
4433
4469
  min-height: 0;
4434
4470
  }
@@ -4450,67 +4486,12 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
4450
4486
  max-height: none;
4451
4487
  overflow-y: auto;
4452
4488
  }
4453
- .conversation-focus-panels #coach-panel {
4454
- flex: 0 0 auto;
4455
- }
4456
- .conversation-focus-panels #coach-panel[open] {
4457
- display: flex;
4458
- flex-direction: column;
4459
- }
4460
- .conversation-focus-panels #coach-panel[open]::details-content {
4461
- min-height: 0;
4462
- display: flex;
4463
- flex-direction: column;
4464
- }
4465
- .conversation-focus-panels #coach-panel[open] > .panel-body {
4466
- max-height: clamp(170px, 25vh, 300px);
4467
- min-height: 0;
4468
- overflow-y: auto;
4469
- }
4470
- .conversation-focus-panels #coach-panel .coach {
4471
- min-height: 0;
4472
- }
4473
- .conversation-focus-panels #coach-panel .coach-input textarea {
4474
- min-height: 68px;
4475
- }
4476
- /* Issue #1657 Treatment 3: #coach-panel's own textarea was extracted into the
4477
- shared #persistent-composer, which openConversationFocus() appends as a
4478
- third sibling of #thread-panel/#coach-panel inside .conversation-focus-panels
4479
- (not nested inside #coach-panel anymore). The #coach-panel-scoped rules
4480
- above/below predate that move and now match nothing real - mirror each one
4481
- for #persistent-composer .coach/.coach-input so collapsing #thread-panel
4482
- still grows the actual textarea, not a since-vacated #coach-panel descendant. */
4483
4489
  .conversation-focus-panels #persistent-composer .coach {
4484
4490
  min-height: 0;
4485
4491
  }
4486
4492
  .conversation-focus-panels #persistent-composer .coach-input textarea {
4487
4493
  min-height: 68px;
4488
4494
  }
4489
- .conversation-focus-panels:has(#coach-panel:not([open])) #thread-panel[open] {
4490
- flex: 1 1 0;
4491
- }
4492
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel[open] {
4493
- flex: 1 1 0;
4494
- }
4495
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel[open]::details-content,
4496
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel[open] > .panel-body,
4497
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel .coach,
4498
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel .coach-input {
4499
- flex: 1 1 0;
4500
- }
4501
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel[open] > .panel-body {
4502
- max-height: none;
4503
- display: flex;
4504
- flex-direction: column;
4505
- }
4506
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel .coach {
4507
- display: flex;
4508
- flex-direction: column;
4509
- }
4510
- .conversation-focus-panels:has(#thread-panel:not([open])) #coach-panel .coach-input textarea {
4511
- flex: 1 1 0;
4512
- min-height: 160px;
4513
- }
4514
4495
  .conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer,
4515
4496
  .conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach,
4516
4497
  .conversation-focus-panels:has(#thread-panel:not([open])) #persistent-composer .coach-input {