fraim-hub 2.0.335 → 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.
- package/dist/src/ai-hub/attachment-store.js +35 -2
- package/dist/src/ai-hub/desktop-main.js +5 -0
- package/dist/src/ai-hub/native-voice-input.js +141 -0
- package/dist/src/ai-hub/server.js +29 -2
- package/package.json +2 -2
- package/public/ai-hub/index.html +26 -16
- package/public/ai-hub/script.js +282 -455
- package/public/ai-hub/styles.css +47 -115
|
@@ -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 (
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
269
|
-
<div class="tree-resizer" id="tree-resizer" role="separator" aria-orientation="vertical" aria-label="Resize
|
|
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
|
-
<!--
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
|
@@ -577,9 +583,13 @@
|
|
|
577
583
|
|
|
578
584
|
<div class="coach-input">
|
|
579
585
|
<input type="file" id="attach-file-input" multiple accept="image/png,image/jpeg,image/gif,.pdf,.txt,.log" hidden>
|
|
586
|
+
<div class="voice-input-status" id="voice-input-status" role="status" aria-live="polite" aria-atomic="true" hidden></div>
|
|
580
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>
|
|
581
588
|
<div class="composer-footer">
|
|
582
589
|
<button class="attach-btn" type="button" id="attach-btn" aria-label="Attach a file or screenshot" title="Attach a file or screenshot">📎</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>
|
|
583
593
|
<div class="quick-coach-row composer-coach-actions" id="quick-coach-btns" hidden>
|
|
584
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">✓</button>
|
|
585
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">⏹</button>
|