fraim-hub 2.0.333 → 2.0.337

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.
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -30,7 +63,6 @@ exports.safeAttachmentMetaPath = safeAttachmentMetaPath;
30
63
  * path its own file-read tool opens.
31
64
  */
32
65
  const path_1 = __importDefault(require("path"));
33
- const sharp_1 = __importDefault(require("sharp"));
34
66
  /** Accepted upload MIME types (spec R1/R11). */
35
67
  exports.ACCEPTED_ATTACHMENT_MIME_TYPES = new Set([
36
68
  'image/png',
@@ -90,8 +122,9 @@ function pdfContainsScriptMarker(buffer) {
90
122
  async function sanitizeAttachmentBuffer(buffer, mimeType) {
91
123
  if (IMAGE_MIME_TYPES.has(mimeType)) {
92
124
  try {
125
+ const sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
93
126
  const format = mimeType === 'image/jpeg' ? 'jpeg' : mimeType === 'image/gif' ? 'gif' : 'png';
94
- return await (0, sharp_1.default)(buffer).toFormat(format).toBuffer();
127
+ return await sharp(buffer).toFormat(format).toBuffer();
95
128
  }
96
129
  catch (err) {
97
130
  throw new AttachmentRejectedError(415, `Not a valid ${mimeType} image.`);
@@ -11,6 +11,7 @@ const electron_1 = require("electron");
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = __importDefault(require("fs"));
13
13
  const server_1 = require("./server");
14
+ const native_voice_input_1 = require("./native-voice-input");
14
15
  const cert_store_1 = require("./cert-store");
15
16
  const office_sideload_1 = require("./office-sideload");
16
17
  const office_setup_policy_1 = require("./office-setup-policy");
@@ -672,6 +673,10 @@ async function launchDesktopShell(options) {
672
673
  return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
673
674
  },
674
675
  restartToLatest: restartToLatestNow,
676
+ voiceInput: (0, native_voice_input_1.createNativeVoiceInputController)({
677
+ getWindow: () => mainWindow,
678
+ sendActionToFirstResponder: (action) => electron_1.Menu.sendActionToFirstResponder(action),
679
+ }),
675
680
  hubMainDiagnostics: {
676
681
  snapshot: () => (0, hub_main_diagnostics_1.getHubMainDiagnosticsSnapshot)(buildDesktopDebugSnapshot(runtimeId)),
677
682
  },
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WINDOWS_VOICE_INPUT_PROGRAM = exports.NativeVoiceInputError = void 0;
7
+ exports.getVoiceInputCapability = getVoiceInputCapability;
8
+ exports.requestWindowsVoiceInput = requestWindowsVoiceInput;
9
+ exports.createNativeVoiceInputController = createNativeVoiceInputController;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ class NativeVoiceInputError extends Error {
13
+ constructor(code) {
14
+ super(code);
15
+ this.code = code;
16
+ this.name = 'NativeVoiceInputError';
17
+ }
18
+ }
19
+ exports.NativeVoiceInputError = NativeVoiceInputError;
20
+ function getVoiceInputCapability(platform = process.platform, native = false) {
21
+ if (platform === 'win32') {
22
+ return {
23
+ platform: 'windows', mode: native ? 'native' : 'manual', shortcutLabel: 'Win+H',
24
+ guidance: 'Press Win+H to start Windows Voice Typing. If it does not appear, check Windows Settings > Privacy & security > Speech and Microphone.',
25
+ };
26
+ }
27
+ if (platform === 'darwin') {
28
+ return {
29
+ platform: 'macos', mode: native ? 'native' : 'manual', shortcutLabel: 'Dictation shortcut',
30
+ guidance: 'Use the microphone key or your configured Dictation shortcut. If Dictation does not appear, enable it in System Settings > Keyboard > Dictation.',
31
+ };
32
+ }
33
+ return { platform: 'other', mode: 'unsupported', guidance: 'Voice input is available through system dictation on Windows and macOS.' };
34
+ }
35
+ // Fixed program only: no renderer or request data enters the native command.
36
+ // The 32-byte union and individually paced events were validated in the #1844 spike.
37
+ exports.WINDOWS_VOICE_INPUT_PROGRAM = String.raw `$ErrorActionPreference = 'Stop'
38
+ $source = @'
39
+ using System;
40
+ using System.ComponentModel;
41
+ using System.Runtime.InteropServices;
42
+ public static class FraimVoiceTyping {
43
+ [StructLayout(LayoutKind.Sequential)]
44
+ private struct INPUT { public uint type; public InputUnion data; }
45
+ [StructLayout(LayoutKind.Explicit, Size = 32)]
46
+ private struct InputUnion { [FieldOffset(0)] public KEYBDINPUT keyboard; }
47
+ [StructLayout(LayoutKind.Sequential)]
48
+ private struct KEYBDINPUT {
49
+ public ushort virtualKey; public ushort scanCode; public uint flags;
50
+ public uint time; public UIntPtr extraInfo;
51
+ }
52
+ [DllImport("user32.dll", SetLastError = true)]
53
+ private static extern uint SendInput(uint count, INPUT[] inputs, int size);
54
+ private static bool Emit(ushort key, uint flags) {
55
+ INPUT input = new INPUT { type = 1, data = new InputUnion {
56
+ keyboard = new KEYBDINPUT { virtualKey = key, flags = flags }
57
+ }};
58
+ return SendInput(1, new INPUT[] { input }, Marshal.SizeOf(typeof(INPUT))) == 1;
59
+ }
60
+ private static void Send(ushort key, uint flags) {
61
+ if (!Emit(key, flags)) throw new Win32Exception(Marshal.GetLastWin32Error());
62
+ }
63
+ public static int Invoke() {
64
+ bool winDown = false, hDown = false;
65
+ try {
66
+ Send(0x5B, 0); winDown = true;
67
+ System.Threading.Thread.Sleep(40);
68
+ Send(0x48, 0); hDown = true;
69
+ System.Threading.Thread.Sleep(40);
70
+ Send(0x48, 2); hDown = false;
71
+ System.Threading.Thread.Sleep(40);
72
+ Send(0x5B, 2); winDown = false;
73
+ return 4;
74
+ } finally {
75
+ // Release each key independently even after partial emission.
76
+ if (hDown) Emit(0x48, 2);
77
+ if (winDown) Emit(0x5B, 2);
78
+ }
79
+ }
80
+ }
81
+ '@
82
+ Add-Type -TypeDefinition $source -Language CSharp
83
+ Write-Output ([FraimVoiceTyping]::Invoke())`;
84
+ function requestWindowsVoiceInput(execute = node_child_process_1.execFile) {
85
+ return new Promise((resolve, reject) => {
86
+ const fail = () => reject(new NativeVoiceInputError('launch_failed'));
87
+ try {
88
+ execute(node_path_1.default.win32.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(exports.WINDOWS_VOICE_INPUT_PROGRAM, 'utf16le').toString('base64')],
89
+ // execFile kills only its child on timeout and calls back after close.
90
+ { windowsHide: true, timeout: 5000, maxBuffer: 4096, encoding: 'utf8' }, (error, stdout) => {
91
+ if (error || stdout.trim() !== '4')
92
+ fail();
93
+ else
94
+ resolve();
95
+ });
96
+ }
97
+ catch {
98
+ fail();
99
+ }
100
+ });
101
+ }
102
+ function createNativeVoiceInputController(options) {
103
+ const capability = getVoiceInputCapability(options.platform ?? process.platform, true);
104
+ let inFlight = false;
105
+ return {
106
+ capability,
107
+ async start() {
108
+ if (capability.mode === 'unsupported')
109
+ throw new NativeVoiceInputError('unsupported_platform');
110
+ if (inFlight)
111
+ throw new NativeVoiceInputError('launch_failed');
112
+ inFlight = true;
113
+ try {
114
+ try {
115
+ const win = options.getWindow();
116
+ if (!win || win.isDestroyed() || win.webContents.isDestroyed())
117
+ throw new Error('unavailable');
118
+ win.show();
119
+ win.focus();
120
+ win.webContents.focus();
121
+ }
122
+ catch {
123
+ throw new NativeVoiceInputError('window_unavailable');
124
+ }
125
+ try {
126
+ if (capability.platform === 'windows')
127
+ await (options.requestWindows ?? requestWindowsVoiceInput)();
128
+ else
129
+ options.sendActionToFirstResponder('startDictation:');
130
+ }
131
+ catch {
132
+ throw new NativeVoiceInputError('launch_failed');
133
+ }
134
+ return { status: 'requested', capability, message: `System dictation requested. ${capability.guidance}` };
135
+ }
136
+ finally {
137
+ inFlight = false;
138
+ }
139
+ },
140
+ };
141
+ }
@@ -52,6 +52,7 @@ const os_1 = __importDefault(require("os"));
52
52
  const crypto_1 = require("crypto");
53
53
  const child_process_1 = require("child_process");
54
54
  const https_1 = __importDefault(require("https"));
55
+ const native_voice_input_1 = require("./native-voice-input");
55
56
  const types_1 = require("../first-run/types");
56
57
  const local_provider_registry_1 = require("../cli/providers/local-provider-registry");
57
58
  const learning_context_builder_1 = require("../local-mcp-server/learning-context-builder");
@@ -2944,6 +2945,7 @@ class AiHubServer {
2944
2945
  this.configuredAgentStore = options.configuredAgentStore || new configured_agents_1.AiHubConfiguredAgentStore();
2945
2946
  this.wordTaskpaneDir = options.wordTaskpaneDir ?? resolveWordTaskpaneDir(this.projectPath);
2946
2947
  this.folderPicker = options.folderPicker ?? pickProjectPath;
2948
+ this.voiceInput = options.voiceInput;
2947
2949
  this.restartToLatest = options.restartToLatest ?? (async () => ({ restarting: false }));
2948
2950
  this.httpsPort = options.httpsPort;
2949
2951
  this.certBundle = options.certBundle;
@@ -3579,6 +3581,7 @@ class AiHubServer {
3579
3581
  // #755: the running build version, so the account menu can show it and flag
3580
3582
  // when a newer version is available (see GET /api/ai-hub/version for `latest`).
3581
3583
  version: (0, version_utils_1.getFraimVersion)(),
3584
+ voiceInput: this.voiceInput?.capability ?? (0, native_voice_input_1.getVoiceInputCapability)(),
3582
3585
  project,
3583
3586
  // Issue #750: `apiKey` is no longer a persisted AiHubPreferences field —
3584
3587
  // it is overlaid on the wire response only, freshly resolved from
@@ -3826,10 +3829,10 @@ class AiHubServer {
3826
3829
  return false;
3827
3830
  }
3828
3831
  }
3829
- requireTrustedHubOrigin(req, res) {
3832
+ requireTrustedHubOrigin(req, res, message = 'Configured agent management is only allowed from the local Hub origin.') {
3830
3833
  if (this.isTrustedHubOrigin(req))
3831
3834
  return true;
3832
- res.status(403).json({ error: 'Configured agent management is only allowed from the local Hub origin.' });
3835
+ res.status(403).json({ error: message });
3833
3836
  return false;
3834
3837
  }
3835
3838
  applyRunHostSessionSignal(run, sessionId, hostIdForCommand = run.hostId) {
@@ -6392,6 +6395,30 @@ class AiHubServer {
6392
6395
  return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not restart to the latest version.' });
6393
6396
  }
6394
6397
  });
6398
+ this.app.post('/api/ai-hub/voice-input/start', async (req, res) => {
6399
+ if (!this.requireTrustedHubOrigin(req, res, 'System dictation is only allowed from the local Hub origin.'))
6400
+ return;
6401
+ const capability = this.voiceInput?.capability ?? (0, native_voice_input_1.getVoiceInputCapability)();
6402
+ const startedAt = Date.now();
6403
+ try {
6404
+ if (capability.mode === 'unsupported')
6405
+ throw new native_voice_input_1.NativeVoiceInputError('unsupported_platform');
6406
+ if (!this.voiceInput || capability.mode === 'manual') {
6407
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.voice_input.manual_fallback', { platform: capability.platform });
6408
+ return res.json({ status: 'manual', capability, message: capability.guidance });
6409
+ }
6410
+ const result = await this.voiceInput.start();
6411
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.voice_input.requested', { platform: capability.platform, mode: capability.mode, durationMs: Date.now() - startedAt });
6412
+ return res.json(result);
6413
+ }
6414
+ catch (error) {
6415
+ const code = error instanceof native_voice_input_1.NativeVoiceInputError ? error.code : 'launch_failed';
6416
+ const status = code === 'unsupported_platform' ? 422 : code === 'window_unavailable' ? 409 : 500;
6417
+ const message = code === 'window_unavailable' ? 'Could not focus the Hub window.' : code === 'launch_failed' ? 'Could not request system dictation.' : '';
6418
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.voice_input.failed', { platform: capability.platform, code, durationMs: Date.now() - startedAt });
6419
+ return res.status(status).json({ code, capability, error: `${message} ${capability.guidance}`.trim() });
6420
+ }
6421
+ });
6395
6422
  this.app.post('/api/ai-hub/project-path/pick', async (_req, res) => {
6396
6423
  try {
6397
6424
  const projectPath = await this.folderPicker();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.333",
3
+ "version": "2.0.337",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "author": "Sid Mathur <sid.mathur@gmail.com>",
6
6
  "homepage": "https://github.com/mathursrus/FRAIM#readme",
@@ -221,7 +221,7 @@
221
221
  "electron-updater": "^6.8.9",
222
222
  "express": "^5.2.1",
223
223
  "extract-zip": "^2.0.1",
224
- "fraim": "2.0.333",
224
+ "fraim": "2.0.337",
225
225
  "mongodb": "^7.0.0",
226
226
  "node-cron": "4.2.1",
227
227
  "node-edge-tts": "^1.2.10",
@@ -93,14 +93,16 @@
93
93
 
94
94
  <!-- Issue #512 / #521 R6: Company area — same shell as Projects (jobs rail + collapsible context/learnings) -->
95
95
  <section class="hub-area" id="area-company" hidden>
96
- <div class="area-shell">
97
- <nav class="area-rail" id="company-rail"></nav>
96
+ <div class="area-shell scope-shell">
97
+ <nav class="tree-sidebar area-rail" id="company-rail" aria-label="Company navigation"></nav>
98
+ <button class="tree-collapse-btn" type="button" aria-label="Collapse navigation" title="Collapse navigation" aria-controls="company-rail" aria-expanded="true"></button>
99
+ <div class="tree-resizer" role="separator" aria-orientation="vertical" aria-label="Resize navigation" aria-controls="company-rail"></div>
98
100
  <div class="area-main">
99
101
  <!-- Info view: context/learnings accordions shown when no conv is active -->
100
102
  <div class="area-info-view" id="company-info-view">
101
103
  <div class="hub-area-page">
102
104
  <div class="eyebrow">FRAIM Hub</div>
103
- <div class="area-h1">Company</div>
105
+ <h1 class="area-h1">Company</h1>
104
106
  <p class="area-lede">Your organization's identity, what you do, how you operate, and the guardrails every employee follows on every job.</p>
105
107
  <div id="company-push-banner"></div>
106
108
  <!-- #744: Brand editor — company identity across the Hub. -->
@@ -152,14 +154,16 @@
152
154
 
153
155
  <!-- Issue #512 / #521 R6: Manager area — same shell as Projects -->
154
156
  <section class="hub-area" id="area-manager" hidden>
155
- <div class="area-shell">
156
- <nav class="area-rail" id="manager-rail"></nav>
157
+ <div class="area-shell scope-shell">
158
+ <nav class="tree-sidebar area-rail" id="manager-rail" aria-label="Manager navigation"></nav>
159
+ <button class="tree-collapse-btn" type="button" aria-label="Collapse navigation" title="Collapse navigation" aria-controls="manager-rail" aria-expanded="true"></button>
160
+ <div class="tree-resizer" role="separator" aria-orientation="vertical" aria-label="Resize navigation" aria-controls="manager-rail"></div>
157
161
  <div class="area-main">
158
162
  <!-- Info view: context/learnings accordions shown when no conv is active -->
159
163
  <div class="area-info-view" id="manager-info-view">
160
164
  <div class="hub-area-page">
161
165
  <div class="eyebrow">FRAIM Hub</div>
162
- <div class="area-h1">Manager</div>
166
+ <h1 class="area-h1">Manager</h1>
163
167
  <p class="area-lede">How you manage: your working style, preferences, and the standing rules your team follows when working with you.</p>
164
168
  <details class="ctx-acc" id="manager-agents-acc" open>
165
169
  <summary><span class="ca-chev">▸</span> <span>AI Agents</span></summary>
@@ -263,18 +267,19 @@
263
267
  </div>
264
268
 
265
269
  <!-- Project workspace: employee tree + the existing conversation surface. -->
266
- <div id="proj-workspace" hidden>
267
- <nav class="tree-sidebar" id="tree-sidebar"></nav>
268
- <button class="tree-collapse-btn" id="tree-collapse-btn" type="button" aria-label="Collapse project navigation" title="Collapse project navigation"></button>
269
- <div class="tree-resizer" id="tree-resizer" role="separator" aria-orientation="vertical" aria-label="Resize project navigation"></div>
270
+ <div id="proj-workspace" class="scope-shell" hidden>
271
+ <nav class="tree-sidebar" id="tree-sidebar" aria-label="Project navigation"></nav>
272
+ <button class="tree-collapse-btn" id="tree-collapse-btn" type="button" aria-label="Collapse navigation" title="Collapse navigation" aria-controls="tree-sidebar" aria-expanded="true"></button>
273
+ <div class="tree-resizer" id="tree-resizer" role="separator" aria-orientation="vertical" aria-label="Resize navigation" aria-controls="tree-sidebar"></div>
270
274
  <div class="workspace-conv">
271
275
 
272
- <!-- #521 R5/R6.4: project brief & learnings as collapsible top sections
273
- (consistent with Company/Manager), replacing the over-conversation
274
- panels. Each sub-area is its own <details> so the manager controls
275
- how much text is shown. The editable brief rows (#proj-brief-profile)
276
- and the learnings list (#project-learnings) are unchanged. -->
277
- <div id="proj-context-top" class="proj-context-top">
276
+ <!-- Project Info uses the same titled page shell as Company and Manager.
277
+ The shared view state makes Info, conversation, and search exclusive. -->
278
+ <div id="proj-context-top" class="area-info-view" hidden>
279
+ <div class="hub-area-page">
280
+ <div class="eyebrow">FRAIM Hub</div>
281
+ <h1 class="area-h1" id="project-info-heading">Project</h1>
282
+ <p class="area-lede">This project's context, rules, learnings, and assignments.</p>
278
283
  <!-- Issue #1198 / #1253 R4 / #1661: the folder this project points at,
279
284
  always on screen. Relocation is an explicit Project Info action. -->
280
285
  <div id="proj-folder-row" class="proj-folder-row" hidden>
@@ -320,6 +325,7 @@
320
325
  </div>
321
326
  </div>
322
327
  </details>
328
+ </div>
323
329
  </div>
324
330
 
325
331
 
@@ -437,12 +443,12 @@
437
443
  </div>
438
444
 
439
445
  <div class="manage-panel-shell">
440
- <details class="panel-details panel-details--thread" id="thread-panel" open>
441
- <summary>
446
+ <section class="panel-details panel-details--thread" id="thread-panel">
447
+ <div class="thread-header">
442
448
  <span class="panel-summary-copy">
443
449
  <span class="panel-kicker" id="thread-panel-kicker">Manage</span>
444
450
  </span>
445
- </summary>
451
+ </div>
446
452
  <!-- Issue #1065: find a passage inside the open conversation (R44-R46). A result that
447
453
  promises "+6 more matches" is an unkept promise without this: the manager lands in
448
454
  a thread that can run to hundreds of messages with no way to reach the match. -->
@@ -473,26 +479,13 @@
473
479
  </div>
474
480
  </div>
475
481
  </div>
476
- </details>
482
+ </section>
477
483
  <div class="manage-header-actions">
478
- <button class="conversation-focus-btn" id="conversation-focus-btn" type="button" aria-label="Expand conversation" title="Expand conversation">&#8599;</button>
479
484
  <button class="micro-manage-toggle" id="micro-manage-toggle" type="button" role="switch" aria-checked="false" aria-controls="manage-body"><span>Micro-Manage</span><span class="micro-switch-track" aria-hidden="true"></span></button>
480
485
  </div>
481
486
  </div>
482
487
 
483
488
  <div class="support-stack">
484
- <!-- Issue #770: single-run deliverables live here (a stable collapsible
485
- panel), not pinned to the bottom of the thread. Static-and-hidden,
486
- populated + toggled by renderDeliverablesPanel. -->
487
- <details class="panel-details panel-details--deliverables" id="deliverables-panel" hidden>
488
- <summary>
489
- <span class="panel-summary-copy">
490
- <span class="panel-kicker">Deliverables</span>
491
- <span class="panel-summary-text" id="deliverables-summary"></span>
492
- </span>
493
- </summary>
494
- <div class="panel-body" id="deliverables-body"></div>
495
- </details>
496
489
  <!-- Issue #1292: when this conversation's own agent stops being available,
497
490
  say so. The selector keeps holding that agent; it is never quietly
498
491
  swapped for another one. Lives outside the composer controls so
@@ -519,23 +512,23 @@
519
512
  </div>
520
513
  </div>
521
514
 
522
- <details class="panel-details panel-details--thread" id="ab-direct-thread-panel" open>
523
- <summary>
515
+ <section class="panel-details panel-details--thread" id="ab-direct-thread-panel">
516
+ <div class="thread-header">
524
517
  <span class="panel-summary-copy">
525
518
  <span class="panel-kicker">Direct output (raw)</span>
526
519
  </span>
527
- </summary>
520
+ </div>
528
521
  <pre class="ab-direct-log" id="ab-direct-log"></pre>
529
- </details>
522
+ </section>
530
523
 
531
524
  <div class="support-stack">
532
- <details class="panel-details panel-details--coach" id="ab-direct-reply-panel" open>
533
- <summary>
525
+ <section class="panel-details panel-details--coach" id="ab-direct-reply-panel">
526
+ <div class="thread-header">
534
527
  <span class="panel-summary-copy">
535
528
  <span class="panel-kicker">Reply to Direct agent</span>
536
- <span class="panel-summary-text" id="ab-direct-reply-summary">Open to send a follow-up</span>
529
+ <span class="panel-summary-text" id="ab-direct-reply-summary">Send a follow-up</span>
537
530
  </span>
538
- </summary>
531
+ </div>
539
532
  <div class="panel-body">
540
533
  <div class="coach">
541
534
  <div class="coach-input">
@@ -544,7 +537,7 @@
544
537
  </div>
545
538
  </div>
546
539
  </div>
547
- </details>
540
+ </section>
548
541
  </div>
549
542
  </div>
550
543
 
@@ -555,9 +548,7 @@
555
548
  visible even when no conversation is active"). Only the content
556
549
  ABOVE this div changes (idle placeholder, hire strip, thread, or
557
550
  the A/B compare panel); the composer itself never opens, closes,
558
- or relocates - except into #conversation-focus-panels during
559
- "expand conversation", handled by openConversationFocus/
560
- closeConversationFocus alongside #thread-panel. -->
551
+ or relocates. -->
561
552
  <div id="persistent-composer">
562
553
  <div class="composer-suggestions" id="composer-suggestions" hidden>
563
554
  <div id="cp-results" class="cp-results-scroll">
@@ -589,16 +580,16 @@
589
580
  <button type="button" id="cp-armed-job-clear" aria-label="Change job">&times;</button>
590
581
  </div>
591
582
  <div class="staged-attachments" id="staged-attachments"></div>
592
- <div class="review-actions" id="review-actions" hidden>
593
- <button class="rb-approve" type="button" id="review-approve">&#10003; Approve</button>
594
- <button class="rb-secondary" type="button" id="review-request-changes">&#8617; Request changes</button>
595
- <span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
596
- </div>
583
+
597
584
  <div class="coach-input">
598
585
  <input type="file" id="attach-file-input" multiple accept="image/png,image/jpeg,image/gif,.pdf,.txt,.log" hidden>
599
- <textarea id="coach-text" aria-label="Type an instruction" placeholder="Type a job name, /handle, or describe what you need..." data-placeholder-idle="Type a job name, /handle, or describe what you need..." data-placeholder-active="Coach this run, or type new instructions..." data-placeholder-armed="Now type what you want done..."></textarea>
586
+ <div class="voice-input-status" id="voice-input-status" role="status" aria-live="polite" aria-atomic="true" hidden></div>
587
+ <textarea id="coach-text" aria-label="Type an instruction" placeholder="Type a job name, /handle, or describe what you need..." data-placeholder-idle="Type a job name, /handle, or describe what you need..." data-placeholder-review="Reply to continue the run, or use Approve / Request changes above..." data-placeholder-active="Coach this run, or type new instructions..." data-placeholder-armed="Now type what you want done..."></textarea>
600
588
  <div class="composer-footer">
601
589
  <button class="attach-btn" type="button" id="attach-btn" aria-label="Attach a file or screenshot" title="Attach a file or screenshot">&#128206;</button>
590
+ <button class="composer-icon-btn voice-input-btn" type="button" id="voice-input-btn" disabled aria-label="Start voice input" aria-describedby="voice-input-status" title="Loading voice input availability">
591
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2M12 19v3M8 22h8"/></svg>
592
+ </button>
602
593
  <div class="quick-coach-row composer-coach-actions" id="quick-coach-btns" hidden>
603
594
  <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>
604
595
  <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>
@@ -683,7 +674,7 @@
683
674
  </main>
684
675
  </div>
685
676
 
686
- <p class="status-line" id="status-line" role="status" aria-live="polite"></p>
677
+
687
678
  </div>
688
679
 
689
680
  <!-- Issue #1065: search results replace the conversation panel while a query is active,
@@ -1130,19 +1121,6 @@
1130
1121
  </div>
1131
1122
  </div>
1132
1123
 
1133
- <!-- Issue #539: Command palette overlay — replaces 2-step modal as job-launch surface -->
1134
- <section class="conversation-focus-overlay" id="conversation-focus-overlay" role="dialog" aria-modal="true" aria-labelledby="conversation-focus-title" hidden>
1135
- <div class="conversation-focus-dialog">
1136
- <div class="conversation-focus-header">
1137
- <div class="conversation-focus-copy">
1138
- <strong id="conversation-focus-title">Focused conversation</strong>
1139
- <span id="conversation-focus-subtitle">Conversation thread and coaching</span>
1140
- </div>
1141
- <button class="conversation-focus-close" id="conversation-focus-close" type="button" aria-label="Close focused conversation">Close</button>
1142
- </div>
1143
- <div class="conversation-focus-panels" id="conversation-focus-panels"></div>
1144
- </div>
1145
- </section>
1146
1124
 
1147
1125
  <!-- Issue #1657 Treatment 3: #cp-modal is retired as a separate dialog.
1148
1126
  The ids below (cp-recent-list, cp-catalog-list, cp-agent-picker, etc.)
@@ -1265,7 +1243,15 @@
1265
1243
  </div>
1266
1244
  </div>
1267
1245
 
1246
+ <div id="review-action-storage" hidden>
1247
+ <div class="review-actions" id="review-actions" hidden>
1248
+ <button class="rb-approve" type="button" id="review-approve">&#10003; Approve</button>
1249
+ <button class="rb-secondary" type="button" id="review-request-changes">&#8617; Request changes</button>
1250
+ <span class="rb-handoff" id="review-handoff">Comment on the doc to leave inline notes, or type below to coach.</span>
1251
+ </div>
1252
+ </div>
1268
1253
  <script src="./script.js?v=dark-status-20260721"></script>
1269
1254
  <script src="./remote-ui-loader.js?v=server-driven-ui-20260724"></script>
1255
+
1270
1256
  </body>
1271
1257
  </html>
@@ -422,8 +422,6 @@
422
422
  .rev-item .rev-dismiss:hover { background: var(--soft); }
423
423
 
424
424
  /* ── #770: Deliverables panel body spacing for the relocated artifact strip. ── */
425
- #deliverables-panel .panel-body { padding-top: 8px; }
426
- #deliverables-panel .rc-artifact-strip { margin: 0; }
427
425
 
428
426
  /* ── R9: project-scoped learnings panel (over the workspace conversation) ── */
429
427
  .proj-learnings-panel {
@@ -633,3 +631,6 @@
633
631
  flex-direction: column;
634
632
  gap: 6px;
635
633
  }
634
+
635
+ .review-turn .review-actions { border: 0; margin: 0; padding: 0; }
636
+ .review-turn .rb-handoff { flex-basis: 100%; }