agy-plugin-porter 0.1.0 → 0.2.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 +59 -4
- package/bin/agy-porter.mjs +2 -2
- package/lib/consent.mjs +122 -0
- package/lib/detector.mjs +2 -1
- package/lib/hook-compiler.mjs +54 -18
- package/lib/index.mjs +3 -0
- package/lib/manifest-sanitizer.mjs +46 -10
- package/lib/mod-compiler.mjs +210 -0
- package/lib/mod-lexer.mjs +143 -0
- package/lib/mod-scan.mjs +169 -0
- package/lib/mod-transform.mjs +84 -0
- package/lib/mode-compiler.mjs +14 -4
- package/lib/rules-compiler.mjs +71 -10
- package/lib/skill-compiler.mjs +4 -1
- package/lib/source.mjs +25 -2
- package/lib/static-auditor.mjs +47 -8
- package/lib/tool-map.mjs +0 -1
- package/lib/transpiler-engine.mjs +50 -18
- package/lib/yaml-lite.mjs +36 -4
- package/package.json +14 -3
- package/scripts/hook-shim.mjs +198 -24
- package/scripts/mod-runtime/claude-code.mjs +20 -0
- package/scripts/mod-runtime/elements.mjs +35 -0
- package/scripts/mod-runtime/engine.mjs +129 -0
- package/scripts/mod-runtime/hook.mjs +24 -0
- package/scripts/mod-runtime/instance.mjs +250 -0
- package/scripts/mod-runtime/lifecycle.mjs +45 -0
- package/scripts/mod-runtime/locate.mjs +48 -0
- package/scripts/mod-runtime/mcp-entry.mjs +55 -0
- package/scripts/mod-runtime/server.mjs +178 -0
- package/scripts/mod-runtime/tool-calls.mjs +83 -0
- package/scripts/mod-runtime/ui-draw.js +122 -0
- package/scripts/mod-runtime/ui.html +61 -0
- package/scripts/mod-runtime/ui.js +149 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Draws a ported mod's element trees ($.ui.resolve's names) as DOM. Text from the mod
|
|
2
|
+
// is always set as text, never parsed as HTML; Markdown escapes before formatting.
|
|
3
|
+
(() => {
|
|
4
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
5
|
+
const inline = (s) => esc(s).replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
|
6
|
+
.replace(/(^|\W)\*([^*]+)\*/g, '$1<i>$2</i>').replace(/\[([^\]]+)\]\((https?:[^)\s"]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
7
|
+
|
|
8
|
+
// Minimal Markdown: headings, lists, code spans, bold, italics, links.
|
|
9
|
+
function markdown(text) {
|
|
10
|
+
const out = [];
|
|
11
|
+
let list = null;
|
|
12
|
+
for (const line of String(text).split('\n')) {
|
|
13
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
14
|
+
const li = /^\s*(?:[-*+]|(\d+)[.)])\s+(.*)$/.exec(line);
|
|
15
|
+
if (li) {
|
|
16
|
+
const tag = li[1] ? 'ol' : 'ul';
|
|
17
|
+
if (list !== tag) { if (list) out.push(`</${list}>`); out.push(li[1] ? `<ol start="${Number(li[1])}">` : '<ul>'); list = tag; }
|
|
18
|
+
out.push(`<li>${inline(li[2])}</li>`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (list) { out.push(`</${list}>`); list = null; }
|
|
22
|
+
if (h) out.push(`<h${h[1].length}>${inline(h[2])}</h${h[1].length}>`);
|
|
23
|
+
else if (line.trim()) out.push(`<p>${inline(line)}</p>`);
|
|
24
|
+
}
|
|
25
|
+
if (list) out.push(`</${list}>`);
|
|
26
|
+
const div = document.createElement('div');
|
|
27
|
+
div.className = 'md';
|
|
28
|
+
div.innerHTML = out.join('');
|
|
29
|
+
return div;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const COLORS = ['cyan', 'green', 'red', 'yellow', 'magenta', 'blue', 'gray', 'grey', 'white', 'black'];
|
|
33
|
+
const SAFE_COLOR = /^(#[0-9a-f]{3,8}|[a-z]+)$/i;
|
|
34
|
+
const textOf = (n) => (typeof n === 'string' ? n : (n.children ?? []).map(textOf).join(''));
|
|
35
|
+
const unit = (v) => `${Number(v) * 0.5}em`;
|
|
36
|
+
const color = (c) => (COLORS.includes(c) ? `var(--${c === 'grey' || c === 'gray' ? 'dim' : c}, ${c})` : SAFE_COLOR.test(c) ? c : '');
|
|
37
|
+
|
|
38
|
+
function draw(node, act, inText = false) {
|
|
39
|
+
if (typeof node === 'string') return document.createTextNode(node);
|
|
40
|
+
const p = node.props ?? {};
|
|
41
|
+
const kids = (el, nested = inText) => { for (const c of node.children ?? []) el.appendChild(draw(c, act, nested)); return el; };
|
|
42
|
+
switch (node.type) {
|
|
43
|
+
case 'Box': {
|
|
44
|
+
const el = document.createElement('div');
|
|
45
|
+
el.style.display = 'flex';
|
|
46
|
+
el.style.flexDirection = p.flexDirection === 'column' ? 'column' : 'row';
|
|
47
|
+
if (p.flexWrap) el.style.flexWrap = String(p.flexWrap);
|
|
48
|
+
for (const k of ['gap', 'columnGap', 'rowGap']) if (p[k] != null) el.style[k] = unit(p[k]);
|
|
49
|
+
if (p.flexGrow != null) el.style.flexGrow = String(Number(p.flexGrow));
|
|
50
|
+
if (p.alignItems) el.style.alignItems = String(p.alignItems);
|
|
51
|
+
if (p.justifyContent) el.style.justifyContent = String(p.justifyContent);
|
|
52
|
+
if (el.style.flexDirection === 'row') el.style.alignItems ||= 'baseline';
|
|
53
|
+
return kids(el, false);
|
|
54
|
+
}
|
|
55
|
+
case 'Text': {
|
|
56
|
+
const el = document.createElement('span');
|
|
57
|
+
el.className = 'Text' + (String(p.wrap ?? '').startsWith('truncate') ? ' trunc' : '');
|
|
58
|
+
if (!inText) el.style.display = 'block';
|
|
59
|
+
if (p.bold) el.style.fontWeight = '600';
|
|
60
|
+
if (p.italic) el.style.fontStyle = 'italic';
|
|
61
|
+
if (p.underline) el.style.textDecoration = 'underline';
|
|
62
|
+
if (p.strikethrough) el.style.textDecoration = 'line-through';
|
|
63
|
+
if (p.dimColor) el.style.color = 'var(--dim)';
|
|
64
|
+
if (typeof p.color === 'string') el.style.color = color(p.color);
|
|
65
|
+
return kids(el, true);
|
|
66
|
+
}
|
|
67
|
+
case 'Button': {
|
|
68
|
+
const el = document.createElement('button');
|
|
69
|
+
el.type = 'button';
|
|
70
|
+
el.className = 'el' + (p.plain ? ' plain' : '') + (p.variant === 'primary' ? ' primary' : '') + (p.dimColor ? ' dim' : '');
|
|
71
|
+
el.textContent = p.label ?? textOf(node);
|
|
72
|
+
if (typeof p.hotkey === 'string') { el.title = `Hotkey: ${p.hotkey}`; el.dataset.hotkey = p.hotkey; }
|
|
73
|
+
if (p.onPress?.$cb) el.onclick = () => act(p.onPress);
|
|
74
|
+
return el;
|
|
75
|
+
}
|
|
76
|
+
case 'Input': {
|
|
77
|
+
const form = document.createElement('form');
|
|
78
|
+
form.className = 'input';
|
|
79
|
+
const input = document.createElement('input');
|
|
80
|
+
input.placeholder = p.placeholder ?? p.label ?? '';
|
|
81
|
+
input.value = p.value ?? '';
|
|
82
|
+
input.setAttribute('aria-label', p.label ?? p.placeholder ?? 'input');
|
|
83
|
+
const btn = document.createElement('button');
|
|
84
|
+
btn.className = 'el';
|
|
85
|
+
btn.textContent = p.submitLabel ?? 'OK';
|
|
86
|
+
form.append(input, btn);
|
|
87
|
+
form.onsubmit = (e) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
const cb = p.onSubmit?.$cb ? p.onSubmit : p.onChange;
|
|
90
|
+
if (cb?.$cb) act(cb, input.value);
|
|
91
|
+
input.value = '';
|
|
92
|
+
};
|
|
93
|
+
return form;
|
|
94
|
+
}
|
|
95
|
+
case 'Select': {
|
|
96
|
+
const sel = document.createElement('select');
|
|
97
|
+
for (const o of p.options ?? []) sel.add(new Option(o.label ?? String(o.value), o.value));
|
|
98
|
+
if (p.value !== undefined) sel.value = p.value;
|
|
99
|
+
const cb = p.onChange ?? p.onSelect ?? p.onSubmit;
|
|
100
|
+
if (cb?.$cb) sel.onchange = () => act(cb, sel.value);
|
|
101
|
+
return sel;
|
|
102
|
+
}
|
|
103
|
+
case 'Code': {
|
|
104
|
+
const pre = document.createElement('pre');
|
|
105
|
+
pre.textContent = p.source ?? textOf(node);
|
|
106
|
+
return pre;
|
|
107
|
+
}
|
|
108
|
+
case 'Markdown':
|
|
109
|
+
return markdown(p.text ?? p.source ?? textOf(node));
|
|
110
|
+
case 'Divider':
|
|
111
|
+
return document.createElement('hr');
|
|
112
|
+
default: {
|
|
113
|
+
const el = document.createElement('div');
|
|
114
|
+
el.dataset.type = String(node.type);
|
|
115
|
+
el.title = `${node.type} isn't drawn by the Antigravity port`;
|
|
116
|
+
return kids(el);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
window.PorterDraw = { draw, markdown };
|
|
122
|
+
})();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Ported mod</title>
|
|
7
|
+
<!-- Loaded first, like science's view, so the host's one-shot theme-change isn't missed.
|
|
8
|
+
preload.js reads the token from this page's URL, so its own URL needs none. -->
|
|
9
|
+
<script src="/preload.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
:root { --bg: #fafafa; --fg: #1f1f1f; --dim: #6b6b6b; --line: rgba(127,127,127,.25); --accent: #0b6bcb;
|
|
12
|
+
--cyan: #0e7490; --green: #15803d; --red: #b91c1c; --yellow: #a16207; --magenta: #a21caf; --blue: #1d4ed8; }
|
|
13
|
+
:root[data-theme="dark"] { --bg: #181818; --fg: #e6e6e6; --dim: #9a9a9a; --accent: #4ea1ff;
|
|
14
|
+
--cyan: #22d3ee; --green: #4ade80; --red: #f87171; --yellow: #facc15; --magenta: #e879f9; --blue: #60a5fa; }
|
|
15
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #181818; --fg: #e6e6e6; --dim: #9a9a9a; --accent: #4ea1ff;
|
|
16
|
+
--cyan: #22d3ee; --green: #4ade80; --red: #f87171; --yellow: #facc15; --magenta: #e879f9; --blue: #60a5fa; } }
|
|
17
|
+
body { font: 13px/1.45 system-ui, sans-serif; margin: 0; background: var(--bg); color: var(--fg); }
|
|
18
|
+
header { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start; padding: 8px 10px; border-bottom: 1px solid var(--line); }
|
|
19
|
+
header .cmd { display: inline-flex; gap: 4px; align-items: center; flex-wrap: wrap; }
|
|
20
|
+
header .cmd label { color: var(--dim); font-size: 12px; }
|
|
21
|
+
header .cmd input { width: 9em; }
|
|
22
|
+
header .cmd .help { color: var(--dim); font-size: 11px; width: 100%; }
|
|
23
|
+
#strips > div { padding: 6px 10px; border-bottom: 1px solid var(--line); }
|
|
24
|
+
nav { display: flex; gap: 2px; padding: 0 10px; border-bottom: 1px solid var(--line); }
|
|
25
|
+
nav button { border: 0; background: none; color: var(--dim); padding: 6px 8px; cursor: pointer; }
|
|
26
|
+
nav button.on { color: var(--fg); border-bottom: 2px solid var(--accent); }
|
|
27
|
+
nav .x { margin-left: 2px; opacity: .6; }
|
|
28
|
+
main { padding: 10px; }
|
|
29
|
+
footer { position: sticky; bottom: 0; padding: 4px 10px; border-top: 1px solid var(--line); color: var(--dim); background: var(--bg); min-height: 1.4em; }
|
|
30
|
+
#toasts { position: fixed; right: 10px; bottom: 34px; display: flex; flex-direction: column; gap: 6px; max-width: 80%; }
|
|
31
|
+
.toast { background: var(--fg); color: var(--bg); padding: 6px 10px; border-radius: 6px; font-size: 12px; }
|
|
32
|
+
.empty { color: var(--dim); padding: 20px 10px; }
|
|
33
|
+
button, input, select { font: inherit; color: inherit; }
|
|
34
|
+
button.el { background: transparent; border: 1px solid var(--line); border-radius: 4px; padding: 2px 8px; cursor: pointer; }
|
|
35
|
+
button.el.primary { border-color: var(--accent); color: var(--accent); }
|
|
36
|
+
button.el.plain { border: 0; padding: 0 2px; text-align: left; }
|
|
37
|
+
button.el.dim { color: var(--dim); }
|
|
38
|
+
button.el:hover { background: var(--line); }
|
|
39
|
+
input, select { background: transparent; border: 1px solid var(--line); border-radius: 4px; padding: 2px 6px; }
|
|
40
|
+
form.input { display: flex; gap: 4px; }
|
|
41
|
+
form.input input { flex: 1; }
|
|
42
|
+
.Text { white-space: pre-wrap; }
|
|
43
|
+
.Text.trunc { white-space: pre; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
44
|
+
pre { background: var(--line); padding: 6px; margin: 0; white-space: pre-wrap; }
|
|
45
|
+
.md p, .md ul, .md ol, .md h1, .md h2, .md h3 { margin: 2px 0; }
|
|
46
|
+
.md h1, .md h2, .md h3 { font-size: 13px; }
|
|
47
|
+
.md code { background: var(--line); padding: 0 3px; border-radius: 3px; }
|
|
48
|
+
.error { color: var(--red); }
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<header id="commands" hidden></header>
|
|
53
|
+
<div id="strips"></div>
|
|
54
|
+
<nav id="tabs" hidden></nav>
|
|
55
|
+
<main id="pane"><div class="empty">Connecting…</div></main>
|
|
56
|
+
<footer id="status"></footer>
|
|
57
|
+
<div id="toasts"></div>
|
|
58
|
+
<script src="/ui-draw.js"></script>
|
|
59
|
+
<script src="/ui.js"></script>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// The side-pane view of a ported Claude Code mod: draws the panes the sidecar sends
|
|
2
|
+
// over SSE, posts clicks back, and reports whether the person can see it.
|
|
3
|
+
(() => {
|
|
4
|
+
const $ = (id) => document.getElementById(id);
|
|
5
|
+
const { draw } = window.PorterDraw;
|
|
6
|
+
const params = new URLSearchParams(location.search);
|
|
7
|
+
const token = params.get('token') || '';
|
|
8
|
+
const raw = params.get('conversationId') || '';
|
|
9
|
+
// A frame with no id, or _new, belongs to a conversation that doesn't exist yet (live).
|
|
10
|
+
const conv = raw && raw !== '_new' ? raw : '';
|
|
11
|
+
const headers = { 'Content-Type': 'application/json', 'X-Sidecar-Token': token };
|
|
12
|
+
const post = (path, body) => fetch(path, { method: 'POST', headers, body: JSON.stringify({ conv, ...body }) }).then((r) => r.json());
|
|
13
|
+
let state = { panes: [], strips: [], commands: [], status: null };
|
|
14
|
+
let active = null;
|
|
15
|
+
let viewId = null;
|
|
16
|
+
|
|
17
|
+
// Host theme: theme-change carries themeType (light/dark).
|
|
18
|
+
window.addEventListener('message', (e) => {
|
|
19
|
+
const parentOrigin = location.ancestorOrigins?.[0];
|
|
20
|
+
if (e.source !== window.parent || !e.origin || e.origin === 'null' || (parentOrigin && parentOrigin !== 'null' && e.origin !== parentOrigin)) return;
|
|
21
|
+
if (e.data?.type === 'theme-change') {
|
|
22
|
+
const t = e.data.payload?.themeType ?? e.data.payload?.theme;
|
|
23
|
+
if (t === 'dark' || t === 'light') document.documentElement.dataset.theme = t;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function toast(text) {
|
|
28
|
+
const el = document.createElement('div');
|
|
29
|
+
el.className = 'toast';
|
|
30
|
+
el.textContent = text;
|
|
31
|
+
$('toasts').appendChild(el);
|
|
32
|
+
setTimeout(() => el.remove(), 6000);
|
|
33
|
+
}
|
|
34
|
+
async function copy(text) {
|
|
35
|
+
try { await navigator.clipboard.writeText(text); return true; } catch {}
|
|
36
|
+
const ta = document.createElement('textarea');
|
|
37
|
+
ta.value = text;
|
|
38
|
+
document.body.appendChild(ta);
|
|
39
|
+
ta.select();
|
|
40
|
+
const ok = document.execCommand('copy');
|
|
41
|
+
ta.remove();
|
|
42
|
+
return ok;
|
|
43
|
+
}
|
|
44
|
+
const status = (text) => { $('status').textContent = text ?? ''; };
|
|
45
|
+
const act = (cb, ...args) => post('/api/action', { cb: cb.$cb, args }).then((r) => { if (r.error) toast(r.error); });
|
|
46
|
+
const note = (className, textContent) => Object.assign(document.createElement('div'), { className, textContent });
|
|
47
|
+
|
|
48
|
+
// Hidden copies (another conversation's frame, Hangar's embedded views) must not
|
|
49
|
+
// count as a placed pane: report visibilityState plus IntersectionObserver.
|
|
50
|
+
let inViewport = false;
|
|
51
|
+
let reported = null;
|
|
52
|
+
function report() {
|
|
53
|
+
const visible = document.visibilityState === 'visible' && inViewport;
|
|
54
|
+
if (viewId === null || visible === reported) return;
|
|
55
|
+
reported = visible;
|
|
56
|
+
post('/api/visibility', { view: viewId, visible }).catch(() => {});
|
|
57
|
+
}
|
|
58
|
+
new IntersectionObserver((entries) => { inViewport = entries.at(-1).isIntersecting; report(); }).observe(document.documentElement);
|
|
59
|
+
document.addEventListener('visibilitychange', report);
|
|
60
|
+
|
|
61
|
+
// One button per command, with a labeled argument field; argumentHint is help text.
|
|
62
|
+
function commandBar() {
|
|
63
|
+
const bar = $('commands');
|
|
64
|
+
bar.replaceChildren();
|
|
65
|
+
(state.commands ?? []).forEach((c, i) => {
|
|
66
|
+
const wrap = document.createElement('span');
|
|
67
|
+
wrap.className = 'cmd';
|
|
68
|
+
const btn = document.createElement('button');
|
|
69
|
+
btn.className = 'el';
|
|
70
|
+
btn.textContent = `/${c.name}`;
|
|
71
|
+
btn.title = c.description ?? '';
|
|
72
|
+
const input = document.createElement('input');
|
|
73
|
+
input.id = `cmd-arg-${i}`;
|
|
74
|
+
const label = Object.assign(document.createElement('label'), { htmlFor: input.id, textContent: 'Arguments' });
|
|
75
|
+
wrap.append(btn, label, input);
|
|
76
|
+
if (c.argumentHint) {
|
|
77
|
+
const help = Object.assign(document.createElement('span'), { id: `cmd-help-${i}`, className: 'help', textContent: `/${c.name} ${c.argumentHint}` });
|
|
78
|
+
input.setAttribute('aria-describedby', help.id);
|
|
79
|
+
wrap.append(help);
|
|
80
|
+
}
|
|
81
|
+
const run = () => post('/api/command', { command: c.name, args: input.value }).then(() => { input.value = ''; });
|
|
82
|
+
btn.onclick = run;
|
|
83
|
+
input.onkeydown = (e) => { if (e.key === 'Enter') run(); };
|
|
84
|
+
bar.appendChild(wrap);
|
|
85
|
+
});
|
|
86
|
+
bar.hidden = !(state.commands ?? []).length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderAll() {
|
|
90
|
+
commandBar();
|
|
91
|
+
$('strips').replaceChildren(...(state.strips ?? []).map((s) => { const d = document.createElement('div'); d.appendChild(draw(s.tree, act)); return d; }));
|
|
92
|
+
const panes = state.panes ?? [];
|
|
93
|
+
if (!panes.some((p) => p.id === active)) active = panes.at(-1)?.id ?? null;
|
|
94
|
+
const tabs = $('tabs');
|
|
95
|
+
tabs.replaceChildren();
|
|
96
|
+
for (const p of panes) {
|
|
97
|
+
const b = document.createElement('button');
|
|
98
|
+
b.className = p.id === active ? 'on' : '';
|
|
99
|
+
b.textContent = p.title;
|
|
100
|
+
b.onclick = () => { active = p.id; renderAll(); };
|
|
101
|
+
const x = document.createElement('span');
|
|
102
|
+
x.className = 'x';
|
|
103
|
+
x.textContent = ' ×';
|
|
104
|
+
x.title = 'Close this pane';
|
|
105
|
+
x.onclick = (e) => { e.stopPropagation(); post('/api/close', { id: p.id }); };
|
|
106
|
+
b.appendChild(x);
|
|
107
|
+
tabs.appendChild(b);
|
|
108
|
+
}
|
|
109
|
+
tabs.hidden = panes.length === 0;
|
|
110
|
+
const pane = panes.find((p) => p.id === active);
|
|
111
|
+
const main = $('pane');
|
|
112
|
+
if (!pane) return main.replaceChildren(note('empty', 'No pane is open. Use a command above, or ask the agent.'));
|
|
113
|
+
if (pane.error) return main.replaceChildren(note('error', pane.error));
|
|
114
|
+
main.replaceChildren(pane.tree ? draw(pane.tree, act) : note('empty', '(empty)'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Hotkeys of the active pane's buttons, when no field has focus.
|
|
118
|
+
document.addEventListener('keydown', (e) => {
|
|
119
|
+
if (e.ctrlKey || e.metaKey || e.altKey || /INPUT|SELECT|TEXTAREA/.test(document.activeElement?.tagName ?? '')) return;
|
|
120
|
+
const btn = $('pane').querySelector(`button[data-hotkey="${CSS.escape(e.key)}"]`);
|
|
121
|
+
if (btn) { e.preventDefault(); btn.click(); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!conv) {
|
|
125
|
+
$('pane').replaceChildren(note('empty', 'No conversation yet. Start one, then reopen this pane.'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const events = new EventSource(`/api/events?token=${encodeURIComponent(token)}&conv=${encodeURIComponent(conv)}`);
|
|
129
|
+
events.onmessage = async (e) => {
|
|
130
|
+
const msg = JSON.parse(e.data);
|
|
131
|
+
if (msg.type === 'hello') { document.title = msg.mod; viewId = msg.view; reported = null; report(); }
|
|
132
|
+
else if (msg.type === 'state') { state = msg; status(msg.status); renderAll(); }
|
|
133
|
+
else if (msg.type === 'status') status(msg.text);
|
|
134
|
+
else if (msg.type === 'toast') toast(msg.text);
|
|
135
|
+
else if (msg.type === 'copy') toast((await copy(msg.text)) ? 'Copied.' : 'Could not copy.');
|
|
136
|
+
else if (msg.type === 'fill') toast((await copy(msg.text)) ? 'Copied. Paste it into the prompt box.' : 'Could not copy the text for the prompt box.');
|
|
137
|
+
};
|
|
138
|
+
events.onerror = () => status('Disconnected from the sidecar. Retrying…');
|
|
139
|
+
|
|
140
|
+
// The workspace is the conversation's cwd until a hook reports it.
|
|
141
|
+
(async () => {
|
|
142
|
+
try {
|
|
143
|
+
const uris = await window.sidecar?.getWorkspaceUris?.();
|
|
144
|
+
const first = Array.isArray(uris) ? uris[0] : null;
|
|
145
|
+
const cwd = first ? decodeURIComponent(new URL(first).pathname).replace(/^\/([A-Za-z]:)/, '$1') : '';
|
|
146
|
+
await post('/api/view', { cwd });
|
|
147
|
+
} catch {}
|
|
148
|
+
})();
|
|
149
|
+
})();
|