aegis-desktop 0.3.0 → 0.4.0
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/README.md +61 -0
- package/lib/deep-link.js +83 -0
- package/lib/local/engine.js +398 -29
- package/lib/local/tools.js +257 -13
- package/lib/quick-launcher.js +81 -0
- package/lib/settings.js +76 -0
- package/lib/sync/sessions.js +27 -0
- package/lib/window-state.js +63 -0
- package/main.js +971 -32
- package/package.json +8 -4
- package/preload.js +121 -0
- package/renderer/app.js +533 -16
- package/renderer/index.html +49 -0
- package/renderer/markdown.js +165 -0
- package/renderer/quick.css +126 -0
- package/renderer/quick.html +39 -0
- package/renderer/quick.js +188 -0
- package/renderer/style.css +341 -0
- package/renderer/vendor/NOTICE.md +20 -0
- package/renderer/vendor/aegis-highlight.js +135 -0
- package/renderer/vendor/marked.umd.js +80 -0
- package/renderer/vendor/purify.min.js +3 -0
- package/vendor/aegis.js +34 -4
package/renderer/index.html
CHANGED
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
<button type="button" id="new-chat" class="ghost-btn">New chat</button>
|
|
27
27
|
</header>
|
|
28
28
|
|
|
29
|
+
<div class="update-banner" id="update-banner" role="status" hidden>
|
|
30
|
+
<span id="update-banner-text"></span>
|
|
31
|
+
<div class="update-banner-actions">
|
|
32
|
+
<button type="button" id="update-download-btn" class="ghost-btn" hidden>Download</button>
|
|
33
|
+
<button type="button" id="update-restart-btn" class="ghost-btn" hidden>Restart to install</button>
|
|
34
|
+
<button type="button" id="update-later-btn" class="ghost-btn">Later</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
29
38
|
<main>
|
|
30
39
|
<aside class="side">
|
|
31
40
|
<section class="card">
|
|
@@ -115,10 +124,46 @@
|
|
|
115
124
|
<p class="hint" id="settings-hint"></p>
|
|
116
125
|
</section>
|
|
117
126
|
|
|
127
|
+
<section class="card">
|
|
128
|
+
<h2>Quick Launcher</h2>
|
|
129
|
+
<label
|
|
130
|
+
class="flow-toggle"
|
|
131
|
+
for="quick-launcher-enabled"
|
|
132
|
+
title="Register a systemwide keyboard shortcut that opens a small always-on-top prompt window from anywhere on the desktop, even when AEGIS Desktop isn't focused. Packaged builds always have it on; this toggle is for a dev run."
|
|
133
|
+
>
|
|
134
|
+
<input type="checkbox" id="quick-launcher-enabled" />
|
|
135
|
+
<span>enable global shortcut</span>
|
|
136
|
+
</label>
|
|
137
|
+
<div class="api-key-row">
|
|
138
|
+
<input
|
|
139
|
+
type="text"
|
|
140
|
+
id="quick-launcher-shortcut"
|
|
141
|
+
placeholder="CmdOrCtrl+Shift+Space"
|
|
142
|
+
autocomplete="off"
|
|
143
|
+
/>
|
|
144
|
+
<button type="button" id="quick-launcher-save" class="ghost-btn">Save</button>
|
|
145
|
+
</div>
|
|
146
|
+
<p class="hint" id="quick-launcher-hint"></p>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<section class="card">
|
|
150
|
+
<h2>Tool approvals</h2>
|
|
151
|
+
<label
|
|
152
|
+
class="flow-toggle"
|
|
153
|
+
for="confirm-mode-toggle"
|
|
154
|
+
title="When on, every exec / writeFile / editFile tool call is previewed and needs your approval (once, for the session, or denied) before it runs. Turn it off to let the agent run those tools without asking."
|
|
155
|
+
>
|
|
156
|
+
<input type="checkbox" id="confirm-mode-toggle" />
|
|
157
|
+
<span>confirm before running tools</span>
|
|
158
|
+
</label>
|
|
159
|
+
<p class="hint" id="confirm-mode-hint"></p>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
118
162
|
<section class="card">
|
|
119
163
|
<h2>Sessions</h2>
|
|
120
164
|
<button type="button" id="sessions-refresh" class="ghost-btn">Refresh</button>
|
|
121
165
|
<button type="button" id="sync-now" class="ghost-btn">Sync now</button>
|
|
166
|
+
<button type="button" id="sessions-export" class="ghost-btn">Export</button>
|
|
122
167
|
<ul class="sessions-list" id="sessions-list"></ul>
|
|
123
168
|
<p class="hint" id="sessions-hint"></p>
|
|
124
169
|
<p class="hint" id="sync-status"></p>
|
|
@@ -284,6 +329,10 @@
|
|
|
284
329
|
</div>
|
|
285
330
|
|
|
286
331
|
<script src="max-tokens.js"></script>
|
|
332
|
+
<script src="vendor/marked.umd.js"></script>
|
|
333
|
+
<script src="vendor/purify.min.js"></script>
|
|
334
|
+
<script src="vendor/aegis-highlight.js"></script>
|
|
335
|
+
<script src="markdown.js"></script>
|
|
287
336
|
<script src="app.js"></script>
|
|
288
337
|
</body>
|
|
289
338
|
</html>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Assistant-message markdown rendering: marked -> DOMPurify -> DOM
|
|
5
|
+
* postprocessing (syntax highlight + copy buttons on fenced code, external
|
|
6
|
+
* link rewiring). This is the ONLY path that may put model output into
|
|
7
|
+
* innerHTML — everywhere else in app.js, assistant/user text stays
|
|
8
|
+
* textContent. See renderInto() below.
|
|
9
|
+
*
|
|
10
|
+
* Loaded as a classic script (CSP script-src 'self', no bundler) after
|
|
11
|
+
* vendor/marked.umd.js, vendor/purify.min.js and vendor/aegis-highlight.js
|
|
12
|
+
* (all under renderer/vendor/ — desktop/vendor/ is a predist.mjs staging
|
|
13
|
+
* dir that the repo's pre-commit hook blocks from ever being committed),
|
|
14
|
+
* before app.js. Exposes window.AegisMarkdown.
|
|
15
|
+
*
|
|
16
|
+
* UMD-lite like aegis-highlight.js: CommonJS export for the pure, DOM-free
|
|
17
|
+
* helpers so they're unit-testable in plain Node; the DOM-dependent half
|
|
18
|
+
* (renderInto) only runs under a real document (Electron renderer).
|
|
19
|
+
*/
|
|
20
|
+
(function (root, factory) {
|
|
21
|
+
const mod = factory();
|
|
22
|
+
if (typeof module === 'object' && module.exports) {
|
|
23
|
+
module.exports = mod;
|
|
24
|
+
} else {
|
|
25
|
+
root.AegisMarkdown = mod;
|
|
26
|
+
}
|
|
27
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
28
|
+
/**
|
|
29
|
+
* Is `href` an absolute http(s) URL — something that could plausibly be
|
|
30
|
+
* opened externally? Deliberately parsed with NO base: a relative path or
|
|
31
|
+
* bare "#anchor" has no scheme and must fail to parse here, not silently
|
|
32
|
+
* inherit https: from a placeholder base (that was the original bug —
|
|
33
|
+
* every relative href came back "external"). This is defense in depth
|
|
34
|
+
* only — main.js's isSafeExternalUrl is the real gate before anything
|
|
35
|
+
* reaches the OS shell.
|
|
36
|
+
*/
|
|
37
|
+
function isExternalHref(href) {
|
|
38
|
+
if (typeof href !== 'string' || !href) return false;
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(href);
|
|
41
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** "language-js" -> "js"; "" for a code block with no fence language. */
|
|
48
|
+
function langFromClassName(className) {
|
|
49
|
+
const m = /language-([\w-]+)/.exec(className || '');
|
|
50
|
+
return m ? m[1] : '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let markedConfigured = false;
|
|
54
|
+
function configureMarked(marked) {
|
|
55
|
+
if (markedConfigured) return;
|
|
56
|
+
marked.setOptions({ gfm: true, breaks: false });
|
|
57
|
+
markedConfigured = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Markdown -> raw (unsanitized) HTML, or null if marked isn't loaded —
|
|
61
|
+
* callers must treat null as "render as plain text", never as HTML. */
|
|
62
|
+
function toHtml(markdownText) {
|
|
63
|
+
const marked = typeof window !== 'undefined' ? window.marked : null;
|
|
64
|
+
if (!marked || typeof marked.parse !== 'function') return null;
|
|
65
|
+
configureMarked(marked);
|
|
66
|
+
return marked.parse(markdownText == null ? '' : String(markdownText));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Raw HTML -> sanitized HTML, or null if DOMPurify isn't loaded/ready. */
|
|
70
|
+
function sanitize(html) {
|
|
71
|
+
const DOMPurify = typeof window !== 'undefined' ? window.DOMPurify : null;
|
|
72
|
+
if (!DOMPurify || typeof DOMPurify.sanitize !== 'function') return null;
|
|
73
|
+
return DOMPurify.sanitize(html, {
|
|
74
|
+
// Restrict link/media URIs to what isExternalHref()/rewireLinks()
|
|
75
|
+
// actually know how to handle; javascript:/data:/file:/mailto: etc.
|
|
76
|
+
// never survive (mailto has no OS-shell path through openExternal,
|
|
77
|
+
// which only allows http/https — see main.js isSafeExternalUrl).
|
|
78
|
+
ALLOWED_URI_REGEXP: /^(?:https?:|#)/i,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Highlight + add a copy button to one fenced-code `<pre>`. Runs on the
|
|
84
|
+
* live sanitized DOM, reading the code's real text (already unescaped by
|
|
85
|
+
* the browser's own HTML parsing) and replacing it with our own
|
|
86
|
+
* self-escaping highlighter output — never re-feeding anything through
|
|
87
|
+
* innerHTML that didn't originate from AegisHighlight.escapeHtml.
|
|
88
|
+
*/
|
|
89
|
+
function decorateCodeBlock(preEl) {
|
|
90
|
+
const codeEl = preEl.querySelector('code');
|
|
91
|
+
if (!codeEl) return;
|
|
92
|
+
const lang = langFromClassName(codeEl.className);
|
|
93
|
+
const raw = codeEl.textContent || '';
|
|
94
|
+
const AegisHighlight = typeof window !== 'undefined' ? window.AegisHighlight : null;
|
|
95
|
+
if (AegisHighlight) {
|
|
96
|
+
codeEl.innerHTML = AegisHighlight.highlight(raw, lang);
|
|
97
|
+
}
|
|
98
|
+
preEl.classList.add('code-block');
|
|
99
|
+
|
|
100
|
+
const btn = document.createElement('button');
|
|
101
|
+
btn.type = 'button';
|
|
102
|
+
btn.className = 'code-copy-btn';
|
|
103
|
+
btn.textContent = 'copy';
|
|
104
|
+
btn.addEventListener('click', () => {
|
|
105
|
+
navigator.clipboard.writeText(raw).then(
|
|
106
|
+
() => {
|
|
107
|
+
btn.textContent = 'copied!';
|
|
108
|
+
setTimeout(() => { btn.textContent = 'copy'; }, 1500);
|
|
109
|
+
},
|
|
110
|
+
() => {
|
|
111
|
+
btn.textContent = 'copy failed';
|
|
112
|
+
setTimeout(() => { btn.textContent = 'copy'; }, 1500);
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
preEl.appendChild(btn);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Every link in rendered markdown opens in the OS default browser via the
|
|
121
|
+
* aegis.openExternal IPC bridge, never as in-app navigation (a stray
|
|
122
|
+
* `<a href>` reaching BrowserWindow's default handler would point the
|
|
123
|
+
* chat UI itself at an arbitrary model-supplied origin).
|
|
124
|
+
*/
|
|
125
|
+
function rewireLinks(container) {
|
|
126
|
+
const aegis = typeof window !== 'undefined' ? window.aegis : null;
|
|
127
|
+
const links = container.querySelectorAll('a[href]');
|
|
128
|
+
for (const a of links) {
|
|
129
|
+
const href = a.getAttribute('href');
|
|
130
|
+
a.removeAttribute('target');
|
|
131
|
+
a.setAttribute('rel', 'noopener noreferrer');
|
|
132
|
+
if (!isExternalHref(href)) {
|
|
133
|
+
// Not absolute http(s) (e.g. an in-page "#" anchor) — strip the
|
|
134
|
+
// href outright rather than let it fall through to default
|
|
135
|
+
// navigation inside the app's own BrowserWindow.
|
|
136
|
+
a.removeAttribute('href');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
a.addEventListener('click', (e) => {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
if (aegis && typeof aegis.openExternal === 'function') aegis.openExternal(href);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render `markdownText` into `containerEl`, replacing its contents.
|
|
148
|
+
* Always sanitized; falls back to a plain-text node (never raw innerHTML)
|
|
149
|
+
* when marked or DOMPurify failed to load. This is the only function in
|
|
150
|
+
* the renderer allowed to put model output into innerHTML.
|
|
151
|
+
*/
|
|
152
|
+
function renderInto(containerEl, markdownText) {
|
|
153
|
+
const html = toHtml(markdownText);
|
|
154
|
+
const safe = html == null ? null : sanitize(html);
|
|
155
|
+
if (safe == null) {
|
|
156
|
+
containerEl.textContent = markdownText == null ? '' : String(markdownText);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
containerEl.innerHTML = safe;
|
|
160
|
+
for (const pre of containerEl.querySelectorAll('pre')) decorateCodeBlock(pre);
|
|
161
|
+
rewireLinks(containerEl);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { renderInto, isExternalHref, langFromClassName };
|
|
165
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0d1117;
|
|
3
|
+
--surface: #12161e;
|
|
4
|
+
--border: #2a3342;
|
|
5
|
+
--text: #d0dfee;
|
|
6
|
+
--text2: #7e9ab8;
|
|
7
|
+
--teal: #00e5c0;
|
|
8
|
+
--red: #ff1744;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html,
|
|
16
|
+
body {
|
|
17
|
+
height: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
background: transparent;
|
|
23
|
+
color: var(--text);
|
|
24
|
+
font: 14px/1.4 -apple-system, 'Segoe UI', system-ui, sans-serif;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ql-shell {
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
height: 100%;
|
|
31
|
+
padding: 12px;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
border: 1px solid var(--border);
|
|
35
|
+
border-radius: 10px;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ql-head {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
color: var(--text2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ql-badge {
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
letter-spacing: 0.04em;
|
|
50
|
+
color: var(--teal);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.ql-model {
|
|
54
|
+
color: var(--text2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.ql-model-select {
|
|
58
|
+
background: var(--surface);
|
|
59
|
+
color: var(--text);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
padding: 2px 4px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ql-spacer {
|
|
67
|
+
flex: 1 1 auto;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ql-stop,
|
|
71
|
+
.ql-push {
|
|
72
|
+
background: var(--surface);
|
|
73
|
+
color: var(--text);
|
|
74
|
+
border: 1px solid var(--border);
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
padding: 4px 8px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ql-stop:hover,
|
|
82
|
+
.ql-push:hover {
|
|
83
|
+
border-color: var(--teal);
|
|
84
|
+
color: var(--teal);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ql-stop {
|
|
88
|
+
border-color: var(--red);
|
|
89
|
+
color: var(--red);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ql-prompt {
|
|
93
|
+
resize: none;
|
|
94
|
+
background: var(--surface);
|
|
95
|
+
color: var(--text);
|
|
96
|
+
border: 1px solid var(--border);
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
padding: 8px;
|
|
99
|
+
font: inherit;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.ql-prompt:focus {
|
|
103
|
+
outline: none;
|
|
104
|
+
border-color: var(--teal);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ql-response {
|
|
108
|
+
flex: 1 1 auto;
|
|
109
|
+
overflow-y: auto;
|
|
110
|
+
white-space: pre-wrap;
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
color: var(--text);
|
|
113
|
+
padding: 0 2px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ql-response:empty::before {
|
|
117
|
+
content: '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ql-foot {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
font-size: 11px;
|
|
125
|
+
color: var(--text2);
|
|
126
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta
|
|
6
|
+
http-equiv="Content-Security-Policy"
|
|
7
|
+
content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:"
|
|
8
|
+
/>
|
|
9
|
+
<title>AEGIS Quick Launcher</title>
|
|
10
|
+
<link rel="stylesheet" href="quick.css" />
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<form id="ql-form" class="ql-shell">
|
|
14
|
+
<div class="ql-head">
|
|
15
|
+
<span class="ql-badge">AEGIS</span>
|
|
16
|
+
<span class="ql-model" id="ql-model-label">Aegis Cloud</span>
|
|
17
|
+
<select id="ql-model-select" class="ql-model-select" title="Model" hidden></select>
|
|
18
|
+
<div class="ql-spacer"></div>
|
|
19
|
+
<button type="button" id="ql-stop" class="ql-stop" hidden>Stop</button>
|
|
20
|
+
</div>
|
|
21
|
+
<textarea
|
|
22
|
+
id="ql-prompt"
|
|
23
|
+
class="ql-prompt"
|
|
24
|
+
rows="2"
|
|
25
|
+
placeholder="Ask AEGIS… (Enter to ask, Esc to close)"
|
|
26
|
+
autocomplete="off"
|
|
27
|
+
></textarea>
|
|
28
|
+
<div class="ql-response" id="ql-response" aria-live="polite"></div>
|
|
29
|
+
<div class="ql-foot">
|
|
30
|
+
<span class="ql-hint" id="ql-status">Enter to ask · Esc to close</span>
|
|
31
|
+
<div class="ql-spacer"></div>
|
|
32
|
+
<button type="button" id="ql-push" class="ql-push" hidden>
|
|
33
|
+
Add to chat (⌘/Ctrl+⏎)
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</form>
|
|
37
|
+
<script src="quick.js"></script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AEGIS Quick Launcher renderer — a one-shot prompt over window.models.chat
|
|
5
|
+
* (the same LocalEngine transport the main window's chat uses), with the
|
|
6
|
+
* agent loop's tool calling turned off (`tools: false`): a small always-
|
|
7
|
+
* on-top popup is the wrong place for a tool-approval card to pop up in.
|
|
8
|
+
*
|
|
9
|
+
* Same contextIsolation/sandbox posture as renderer/app.js — talks only to
|
|
10
|
+
* window.models / window.quickLauncher, both whitelisted by preload.js.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
(function () {
|
|
14
|
+
if (!window.models || !window.quickLauncher) {
|
|
15
|
+
document.body.textContent =
|
|
16
|
+
'This page must run inside the AEGIS Electron host (window.models/window.quickLauncher missing).';
|
|
17
|
+
throw new Error('window.models/window.quickLauncher unavailable — not running under Electron');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const models = window.models;
|
|
21
|
+
const quickLauncher = window.quickLauncher;
|
|
22
|
+
|
|
23
|
+
const CLASS = 'aegis';
|
|
24
|
+
const MODEL_KEY = 'quick-launcher-model';
|
|
25
|
+
const IDLE_HINT = 'Enter to ask · Esc to close';
|
|
26
|
+
|
|
27
|
+
const els = {
|
|
28
|
+
form: document.getElementById('ql-form'),
|
|
29
|
+
prompt: document.getElementById('ql-prompt'),
|
|
30
|
+
response: document.getElementById('ql-response'),
|
|
31
|
+
modelLabel: document.getElementById('ql-model-label'),
|
|
32
|
+
modelSelect: document.getElementById('ql-model-select'),
|
|
33
|
+
stopBtn: document.getElementById('ql-stop'),
|
|
34
|
+
pushBtn: document.getElementById('ql-push'),
|
|
35
|
+
status: document.getElementById('ql-status'),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let busy = false;
|
|
39
|
+
let sessionId = null;
|
|
40
|
+
let lastPrompt = '';
|
|
41
|
+
let lastResponse = '';
|
|
42
|
+
let hasResult = false;
|
|
43
|
+
|
|
44
|
+
function setStatus(text) {
|
|
45
|
+
els.status.textContent = text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setBusy(next) {
|
|
49
|
+
busy = next;
|
|
50
|
+
els.stopBtn.hidden = !next;
|
|
51
|
+
els.prompt.disabled = next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Populate the model picker for the fixed 'aegis' class. Degrades to a
|
|
55
|
+
* plain label (no picker) when there is no key configured or the catalog
|
|
56
|
+
* call fails — the launcher still opens, it just can't send anything. */
|
|
57
|
+
async function loadModel() {
|
|
58
|
+
try {
|
|
59
|
+
const classes = (await models.listClasses()) || [];
|
|
60
|
+
const aegisClass = classes.find((c) => c.class === CLASS);
|
|
61
|
+
if (!aegisClass || !aegisClass.configured) {
|
|
62
|
+
els.modelLabel.textContent = 'Aegis Cloud — no key configured';
|
|
63
|
+
els.modelSelect.hidden = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const data = await models.listModels(CLASS);
|
|
67
|
+
const list = (data && data.models) || [];
|
|
68
|
+
els.modelSelect.innerHTML = '';
|
|
69
|
+
for (const m of list) {
|
|
70
|
+
const opt = document.createElement('option');
|
|
71
|
+
opt.value = m.id;
|
|
72
|
+
opt.textContent = m.label || m.id;
|
|
73
|
+
els.modelSelect.appendChild(opt);
|
|
74
|
+
}
|
|
75
|
+
const saved = localStorage.getItem(MODEL_KEY);
|
|
76
|
+
if (saved && list.some((m) => m.id === saved)) {
|
|
77
|
+
els.modelSelect.value = saved;
|
|
78
|
+
}
|
|
79
|
+
els.modelSelect.hidden = list.length <= 1;
|
|
80
|
+
els.modelLabel.textContent = 'Aegis Cloud';
|
|
81
|
+
} catch {
|
|
82
|
+
els.modelLabel.textContent = 'Aegis Cloud — unavailable';
|
|
83
|
+
els.modelSelect.hidden = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
els.modelSelect.addEventListener('change', () => {
|
|
88
|
+
localStorage.setItem(MODEL_KEY, els.modelSelect.value);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function ask(prompt) {
|
|
92
|
+
if (!prompt || busy) return;
|
|
93
|
+
hasResult = false;
|
|
94
|
+
els.pushBtn.hidden = true;
|
|
95
|
+
els.response.textContent = '';
|
|
96
|
+
setBusy(true);
|
|
97
|
+
setStatus('thinking…');
|
|
98
|
+
sessionId = `quick-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
99
|
+
let text = '';
|
|
100
|
+
try {
|
|
101
|
+
const res = await models.chat(
|
|
102
|
+
{
|
|
103
|
+
class: CLASS,
|
|
104
|
+
model: els.modelSelect.value || undefined,
|
|
105
|
+
prompt,
|
|
106
|
+
tools: false,
|
|
107
|
+
sessionId,
|
|
108
|
+
},
|
|
109
|
+
(chunk) => {
|
|
110
|
+
if (chunk && typeof chunk.delta === 'string' && chunk.delta) {
|
|
111
|
+
text += chunk.delta;
|
|
112
|
+
els.response.textContent = text;
|
|
113
|
+
els.response.scrollTop = els.response.scrollHeight;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
const msg = res && res.choices && res.choices[0] && res.choices[0].message;
|
|
118
|
+
const finalText = text || (msg && msg.content) || '';
|
|
119
|
+
els.response.textContent = finalText;
|
|
120
|
+
lastPrompt = prompt;
|
|
121
|
+
lastResponse = finalText;
|
|
122
|
+
hasResult = Boolean(finalText);
|
|
123
|
+
els.pushBtn.hidden = !hasResult;
|
|
124
|
+
setStatus(hasResult ? '⌘/Ctrl+⏎ to add this to the main chat' : 'no answer');
|
|
125
|
+
} catch (err) {
|
|
126
|
+
els.response.textContent = `Error: ${err && err.message ? err.message : err}`;
|
|
127
|
+
setStatus('failed');
|
|
128
|
+
} finally {
|
|
129
|
+
setBusy(false);
|
|
130
|
+
sessionId = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stop() {
|
|
135
|
+
if (sessionId) models.cancel(sessionId).catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pushToMain() {
|
|
139
|
+
if (!hasResult) return;
|
|
140
|
+
quickLauncher
|
|
141
|
+
.pushToMain({
|
|
142
|
+
prompt: lastPrompt,
|
|
143
|
+
response: lastResponse,
|
|
144
|
+
model: els.modelSelect.value || null,
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
els.form.addEventListener('submit', (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
ask(els.prompt.value.trim());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
els.prompt.addEventListener('keydown', (e) => {
|
|
155
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
pushToMain();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
ask(els.prompt.value.trim());
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
els.stopBtn.addEventListener('click', stop);
|
|
167
|
+
els.pushBtn.addEventListener('click', pushToMain);
|
|
168
|
+
|
|
169
|
+
// The window is shown/hidden, not reloaded, on every shortcut press (see
|
|
170
|
+
// main.js toggleQuickLauncher) — so each time it regains focus is a fresh
|
|
171
|
+
// "session" from the user's point of view and should start clean, unless a
|
|
172
|
+
// stream from the previous session is still running in the background.
|
|
173
|
+
window.addEventListener('focus', () => {
|
|
174
|
+
if (busy) {
|
|
175
|
+
els.prompt.focus();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
els.prompt.value = '';
|
|
179
|
+
els.response.textContent = '';
|
|
180
|
+
els.pushBtn.hidden = true;
|
|
181
|
+
hasResult = false;
|
|
182
|
+
setStatus(IDLE_HINT);
|
|
183
|
+
els.prompt.focus();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
loadModel();
|
|
187
|
+
els.prompt.focus();
|
|
188
|
+
})();
|