|
|
… |
57 unchanged lines |
| 58 |
58 |
|
); |
| 59 |
59 |
|
const brief = this.model.briefs.projects.find((b) => b.project === this.name)?.text; |
| 60 |
60 |
|
const purpose = this.model.channels.find((c) => c.name === this.name)?.purpose ?? ""; |
| 61 |
|
- |
void this.panel.webview.postMessage({ type: "render", html: render(m.items, thread?.items, this.openThread, replies), brief: brief ?? "", purpose }); |
|
61 |
+ |
// Names for @-completion: every agent, its project, presence; @channel, @here, @all. |
|
62 |
+ |
const names = this.model.agents |
|
63 |
+ |
.filter((a) => !a.disabled) |
|
64 |
+ |
.map((a) => ({ name: a.name, detail: `${a.kind} · #${a.project} · ${a.presence}` })) |
|
65 |
+ |
.concat([ |
|
66 |
+ |
{ name: "channel", detail: "every agent who belongs in this channel" }, |
|
67 |
+ |
{ name: "here", detail: "every agent online in this channel" }, |
|
68 |
+ |
{ name: "all", detail: "every agent, everywhere" }, |
|
69 |
+ |
]); |
|
70 |
+ |
void this.panel.webview.postMessage({ type: "render", html: render(m.items, thread?.items, this.openThread, replies), brief: brief ?? "", purpose, names }); |
| 62 |
71 |
|
} catch (e) { |
| 63 |
72 |
|
void this.panel.webview.postMessage({ type: "error", text: (e as Error).message }); |
| 64 |
73 |
|
} |
|
|
… |
76 unchanged lines |
| 141 |
150 |
|
.thread { margin: 10px 0 4px 16px; padding-left: 10px; border-left: 2px solid var(--vscode-textLink-foreground); } |
| 142 |
151 |
|
.thead { font-weight: 600; margin-bottom: 4px; } |
| 143 |
152 |
|
.empty, .error { color: var(--vscode-descriptionForeground); padding: 12px 0; } .error { color: var(--vscode-errorForeground); } |
| 144 |
|
- |
form { display: flex; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--vscode-panel-border); } |
|
153 |
+ |
form { position: relative; display: flex; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--vscode-panel-border); } |
|
154 |
+ |
#ac { position: absolute; left: 16px; bottom: 100%; margin-bottom: 2px; min-width: 280px; max-height: 220px; overflow-y: auto; background: var(--vscode-editorSuggestWidget-background); border: 1px solid var(--vscode-editorSuggestWidget-border); color: var(--vscode-editorSuggestWidget-foreground); z-index: 10; } |
|
155 |
+ |
#ac[hidden] { display: none; } |
|
156 |
+ |
#ac div { padding: 3px 8px; cursor: pointer; display: flex; gap: 10px; justify-content: space-between; } |
|
157 |
+ |
#ac div.on { background: var(--vscode-editorSuggestWidget-selectedBackground); color: var(--vscode-editorSuggestWidget-selectedForeground, inherit); } |
|
158 |
+ |
#ac .d { color: var(--vscode-descriptionForeground); font-size: 11px; } |
| 145 |
159 |
|
textarea { flex: 1; min-height: 44px; max-height: 200px; resize: vertical; font: inherit; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); padding: 6px 8px; } |
| 146 |
160 |
|
button { background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: 0; padding: 6px 14px; cursor: pointer; align-self: flex-end; } |
| 147 |
161 |
|
button:disabled { opacity: .6; } |
| 148 |
162 |
|
</style></head><body> |
| 149 |
163 |
|
<header><h1>${esc(title)}</h1><div class="purpose" id="purpose"></div><details class="brief" id="brief" hidden><summary>Project brief · <a id="edit">edit</a></summary><pre id="brieftext"></pre></details></header> |
| 150 |
164 |
|
<div id="log"><p class="empty">Loading…</p></div> |
| 151 |
|
- |
<form id="f"><textarea id="t" placeholder="Write as yourself. @name mentions an agent. Ctrl+Enter sends."></textarea><button id="b" type="submit">Send</button></form> |
|
165 |
+ |
<form id="f"><div id="ac" role="listbox" hidden></div><textarea id="t" placeholder="Write as yourself. @name mentions an agent. Ctrl+Enter sends."></textarea><button id="b" type="submit">Send</button></form> |
| 152 |
166 |
|
<script nonce="${nonce}"> |
| 153 |
167 |
|
const vscode = acquireVsCodeApi(); |
| 154 |
168 |
|
const log = document.getElementById("log"), t = document.getElementById("t"), b = document.getElementById("b"); |
| 155 |
169 |
|
let thread = ""; |
|
170 |
+ |
// @-completion of agent names. |
|
171 |
+ |
const ac = document.getElementById("ac"); |
|
172 |
+ |
let names = [], shown = [], sel = 0, at = -1; |
|
173 |
+ |
function token() { |
|
174 |
+ |
const upto = t.value.slice(0, t.selectionStart); |
|
175 |
+ |
const m = /(^|[\s(])@([a-z0-9-]*)$/i.exec(upto); |
|
176 |
+ |
return m ? { start: upto.length - m[2].length - 1, word: m[2].toLowerCase() } : null; |
|
177 |
+ |
} |
|
178 |
+ |
function close() { ac.hidden = true; shown = []; at = -1; } |
|
179 |
+ |
function draw() { |
|
180 |
+ |
ac.innerHTML = ""; |
|
181 |
+ |
shown.forEach((n, i) => { |
|
182 |
+ |
const d = document.createElement("div"); |
|
183 |
+ |
d.className = i === sel ? "on" : ""; |
|
184 |
+ |
d.setAttribute("role", "option"); |
|
185 |
+ |
const a = document.createElement("span"); a.textContent = "@" + n.name; |
|
186 |
+ |
const b = document.createElement("span"); b.className = "d"; b.textContent = n.detail; |
|
187 |
+ |
d.append(a, b); |
|
188 |
+ |
d.addEventListener("mousedown", (e) => { e.preventDefault(); pick(i); }); |
|
189 |
+ |
ac.appendChild(d); |
|
190 |
+ |
}); |
|
191 |
+ |
ac.hidden = !shown.length; |
|
192 |
+ |
const on = ac.children[sel]; if (on) on.scrollIntoView({ block: "nearest" }); |
|
193 |
+ |
} |
|
194 |
+ |
function suggest() { |
|
195 |
+ |
const tk = token(); |
|
196 |
+ |
if (!tk) return close(); |
|
197 |
+ |
at = tk.start; |
|
198 |
+ |
const w = tk.word; |
|
199 |
+ |
const starts = names.filter((n) => n.name.startsWith(w)); |
|
200 |
+ |
const parts = names.filter((n) => !n.name.startsWith(w) && n.name.split("-").some((p) => p.startsWith(w))); |
|
201 |
+ |
shown = starts.concat(parts).slice(0, 12); |
|
202 |
+ |
sel = 0; |
|
203 |
+ |
draw(); |
|
204 |
+ |
} |
|
205 |
+ |
function pick(i) { |
|
206 |
+ |
const n = shown[i]; if (!n || at < 0) return; |
|
207 |
+ |
const end = t.selectionStart; |
|
208 |
+ |
t.value = t.value.slice(0, at) + "@" + n.name + " " + t.value.slice(end); |
|
209 |
+ |
const caret = at + n.name.length + 2; |
|
210 |
+ |
t.setSelectionRange(caret, caret); |
|
211 |
+ |
close(); t.focus(); |
|
212 |
+ |
} |
|
213 |
+ |
t.addEventListener("input", suggest); |
|
214 |
+ |
t.addEventListener("click", suggest); |
|
215 |
+ |
t.addEventListener("blur", () => setTimeout(close, 100)); |
|
216 |
+ |
t.addEventListener("keydown", (e) => { |
|
217 |
+ |
if (ac.hidden) return; |
|
218 |
+ |
if (e.key === "ArrowDown") { e.preventDefault(); sel = (sel + 1) % shown.length; draw(); } |
|
219 |
+ |
else if (e.key === "ArrowUp") { e.preventDefault(); sel = (sel - 1 + shown.length) % shown.length; draw(); } |
|
220 |
+ |
else if ((e.key === "Enter" && !e.ctrlKey && !e.metaKey) || e.key === "Tab") { e.preventDefault(); e.stopImmediatePropagation(); pick(sel); } |
|
221 |
+ |
else if (e.key === "Escape") { e.preventDefault(); close(); } |
|
222 |
+ |
}, true); |
| 156 |
223 |
|
function send(e) { if (e) e.preventDefault(); const body = t.value.trim(); if (!body) return; b.disabled = true; vscode.postMessage({ type: "post", body, thread: thread || undefined }); } |
| 157 |
224 |
|
document.getElementById("f").addEventListener("submit", send); |
| 158 |
|
- |
t.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) send(e); }); |
|
225 |
+ |
t.addEventListener("keydown", (e) => { if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { close(); send(e); } }); |
| 159 |
226 |
|
document.getElementById("edit").addEventListener("click", (e) => { e.preventDefault(); vscode.postMessage({ type: "brief" }); }); |
| 160 |
227 |
|
log.addEventListener("click", (e) => { const a = e.target.closest("[data-thread]"); if (!a) return; thread = a.getAttribute("data-thread"); t.placeholder = thread ? "Reply in the thread. Ctrl+Enter sends." : "Write as yourself. @name mentions an agent. Ctrl+Enter sends."; vscode.postMessage({ type: "thread", thread }); }); |
| 161 |
228 |
|
window.addEventListener("message", (ev) => { |
|
|
… |
2 unchanged lines |
| 164 |
231 |
|
const atEnd = log.scrollTop + log.clientHeight >= log.scrollHeight - 30; |
| 165 |
232 |
|
log.innerHTML = m.html; |
| 166 |
233 |
|
document.getElementById("purpose").textContent = m.purpose || ""; |
|
234 |
+ |
if (m.names) names = m.names; |
| 167 |
235 |
|
const br = document.getElementById("brief"); br.hidden = !m.brief; document.getElementById("brieftext").textContent = m.brief || ""; |
| 168 |
236 |
|
if (atEnd) log.scrollTop = log.scrollHeight; |
| 169 |
237 |
|
} else if (m.type === "posted") { t.value = ""; b.disabled = false; t.focus(); } |
|
|
… |
5 unchanged lines |