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.
- package/dist/src/ai-hub/preferences.js +1 -10
- package/dist/src/ai-hub/server.js +1 -1
- package/package.json +2 -2
- package/public/ai-hub/index.html +83 -108
- package/public/ai-hub/review.css +1 -2
- package/public/ai-hub/script.js +136 -185
- package/public/ai-hub/styles.css +118 -137
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -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
|
|
481
|
-
coaching
|
|
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 ▾</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
|
|
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">×</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">✓ Approve</button>
|
|
591
|
+
<button class="rb-secondary" type="button" id="review-request-changes">↩ 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">📎</button>
|
|
695
596
|
<input type="file" id="attach-file-input" multiple accept="image/png,image/jpeg,image/gif,.pdf,.txt,.log" hidden>
|
|
696
|
-
<
|
|
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">✓</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">⏹</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">👍</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">👎</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">🧭</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 MB each.</div>
|
package/public/ai-hub/review.css
CHANGED
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
.rc-art-btn--teach:hover { background: #ece3ff; }
|
|
41
41
|
|
|
42
|
-
/*
|
|
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;
|
package/public/ai-hub/script.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
2072
|
-
//
|
|
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.
|
|
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 (
|
|
2170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3637
|
-
|
|
3638
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
4083
|
+
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
|
|
4076
4084
|
requestAnimationFrame(() => {
|
|
4077
4085
|
syncThreadMessageViewport();
|
|
4078
|
-
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage
|
|
4086
|
+
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
|
|
4079
4087
|
});
|
|
4080
4088
|
setTimeout(() => {
|
|
4081
4089
|
syncThreadMessageViewport();
|
|
4082
|
-
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage
|
|
4090
|
+
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage, reviewFollow, clearedPendingDeliveryMessages);
|
|
4083
4091
|
});
|
|
4084
4092
|
setTimeout(() => {
|
|
4085
4093
|
syncThreadMessageViewport();
|
|
4086
|
-
scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, hasNewPendingDeliveryMessage
|
|
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
|
-
|
|
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
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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.
|
|
8268
|
-
//
|
|
8269
|
-
//
|
|
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
|
-
|
|
8306
|
+
syncComposerPlaceholder();
|
|
8330
8307
|
} else {
|
|
8331
8308
|
chip.hidden = true;
|
|
8332
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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', () => {
|
package/public/ai-hub/styles.css
CHANGED
|
@@ -1469,12 +1469,10 @@ img.conv-employee-avatar {
|
|
|
1469
1469
|
border-color: var(--accent);
|
|
1470
1470
|
color: var(--accent-strong);
|
|
1471
1471
|
}
|
|
1472
|
-
.coach
|
|
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
|
-
/*
|
|
1590
|
-
|
|
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
|
|
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
|
-
/*
|
|
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:
|
|
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
|
|
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
|
-
.
|
|
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:
|
|
4324
|
+
min-height: 92px;
|
|
4303
4325
|
padding-top: 9px;
|
|
4304
|
-
padding-
|
|
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;
|
|
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 {
|