@insitue/sdk 0.2.0 → 0.3.2

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,1236 +0,0 @@
1
- import {
2
- A,
3
- PROTOCOL_VERSION,
4
- R,
5
- beginPick,
6
- buildBundle,
7
- d,
8
- getCaptureSettings,
9
- installRuntimeCollectors,
10
- k,
11
- onCaptureSettingsChange,
12
- onDisplayMediaChange,
13
- retryDisplayMedia,
14
- runtimeErrorCount,
15
- setCaptureSettings,
16
- stopDisplayMedia,
17
- y
18
- } from "./chunk-62N4L6RA.js";
19
-
20
- // src/client.ts
21
- var CompanionClient = class {
22
- constructor(port, events) {
23
- this.port = port;
24
- this.events = events;
25
- this.base = `http://127.0.0.1:${port}`;
26
- this.wsUrl = `ws://127.0.0.1:${port}`;
27
- }
28
- port;
29
- events;
30
- ws = null;
31
- base;
32
- wsUrl;
33
- pending = /* @__PURE__ */ new Map();
34
- async connect() {
35
- this.events.onState("connecting");
36
- let token;
37
- try {
38
- const res = await fetch(`${this.base}/insitu/handshake`);
39
- if (!res.ok) throw new Error(`handshake ${res.status}`);
40
- token = (await res.json()).token;
41
- } catch (e) {
42
- this.events.onState("error", `companion unreachable (run \`npx insitue\`)`);
43
- return;
44
- }
45
- await new Promise((done) => {
46
- const ws = new WebSocket(this.wsUrl);
47
- this.ws = ws;
48
- ws.onopen = () => ws.send(
49
- JSON.stringify({ t: "hello", protocolVersion: PROTOCOL_VERSION, token })
50
- );
51
- ws.onmessage = (ev) => {
52
- const msg = JSON.parse(ev.data);
53
- if (msg.t === "hello-ok") {
54
- this.events.onState("connected", `companion ${msg.companionVersion}`);
55
- done();
56
- } else if (msg.t === "pong") {
57
- const cb = this.pending.get(msg.nonce);
58
- if (cb) {
59
- this.pending.delete(msg.nonce);
60
- cb(performance.now() - Number(msg.nonce.split(":")[1]));
61
- }
62
- } else if (msg.t === "agent-status") {
63
- this.events.onAgentStatus?.({
64
- ready: msg.ready,
65
- transport: msg.transport,
66
- warnings: msg.warnings,
67
- blockers: msg.blockers
68
- });
69
- } else if (msg.t === "agent-stream") {
70
- this.events.onAgentEvent?.(msg.event);
71
- } else if (msg.t === "changeset-proposed") {
72
- this.events.onChangeset?.(msg.turnId, msg.files);
73
- } else if (msg.t === "changeset-applied") {
74
- this.events.onApplied?.(
75
- msg.turnId,
76
- msg.files,
77
- msg.checkpointRef
78
- );
79
- } else if (msg.t === "agent-undone") {
80
- this.events.onUndone?.(msg.turnId, msg.restored);
81
- } else if (msg.t === "agent-session-undone") {
82
- this.events.onSessionUndone?.(msg.restored);
83
- } else if (msg.t === "agent-session-committed") {
84
- this.events.onSessionCommitted?.(msg.commit, msg.files);
85
- } else if (msg.t === "capture-resolved") {
86
- this.events.onResolved?.(msg.id, msg.resolved, msg.note);
87
- } else if (msg.t === "subscribers-attached") {
88
- this.events.onSubscribersAttached?.(msg.count);
89
- } else if (msg.t === "error") {
90
- this.events.onState("error", `${msg.code}: ${msg.message}`);
91
- done();
92
- }
93
- };
94
- ws.onerror = () => {
95
- this.events.onState("error", "websocket error");
96
- done();
97
- };
98
- ws.onclose = () => {
99
- if (this.ws === ws) this.events.onState("idle");
100
- };
101
- });
102
- }
103
- ping() {
104
- return new Promise((resolve, reject) => {
105
- const ws = this.ws;
106
- if (!ws || ws.readyState !== WebSocket.OPEN) {
107
- reject(new Error("not connected"));
108
- return;
109
- }
110
- const nonce = `n:${performance.now()}:${Math.random()}`;
111
- this.pending.set(nonce, resolve);
112
- ws.send(JSON.stringify({ t: "ping", nonce }));
113
- setTimeout(() => {
114
- if (this.pending.delete(nonce)) reject(new Error("ping timeout"));
115
- }, 3e3);
116
- });
117
- }
118
- submitCapture(bundle) {
119
- const ws = this.ws;
120
- if (!ws || ws.readyState !== WebSocket.OPEN) return false;
121
- ws.send(JSON.stringify({ t: "capture", bundle }));
122
- return true;
123
- }
124
- sendTurn(turnId, bundleId, userMessage) {
125
- const ws = this.ws;
126
- if (!ws || ws.readyState !== WebSocket.OPEN) return false;
127
- ws.send(
128
- JSON.stringify({ t: "agent-turn", turnId, bundleId, userMessage })
129
- );
130
- return true;
131
- }
132
- /** #162: route the user's typed intent to a CLI/MCP subscriber
133
- * (e.g. claude with `/insitue:connect` open) instead of the
134
- * in-overlay headless agent. No reply arrives back through this
135
- * client — the external claude shows its work in its own
136
- * terminal. Callers should switch the panel UI to a "Sent to
137
- * claude in your terminal" affordance after this returns. */
138
- sendAsk(turnId, bundleId, text) {
139
- const ws = this.ws;
140
- if (!ws || ws.readyState !== WebSocket.OPEN) return false;
141
- ws.send(
142
- JSON.stringify({ t: "agent-ask-external", turnId, bundleId, text })
143
- );
144
- return true;
145
- }
146
- cancelTurn(turnId) {
147
- this.ws?.send(JSON.stringify({ t: "agent-cancel", turnId }));
148
- }
149
- sendDecision(turnId, decision, files, reason) {
150
- this.ws?.send(
151
- JSON.stringify({ t: "agent-decision", turnId, decision, files, reason })
152
- );
153
- }
154
- sendUndo(turnId) {
155
- this.ws?.send(JSON.stringify({ t: "agent-undo", turnId }));
156
- }
157
- sendUndoSession() {
158
- this.ws?.send(JSON.stringify({ t: "agent-undo-session" }));
159
- }
160
- sendCommitSession(message) {
161
- this.ws?.send(JSON.stringify({ t: "agent-commit-session", message }));
162
- }
163
- dispose() {
164
- this.ws?.close();
165
- this.ws = null;
166
- }
167
- };
168
-
169
- // src/overlay.ts
170
- var DOT = {
171
- idle: "#888",
172
- connecting: "#e0a30c",
173
- connected: "#2fd16b",
174
- error: "#ff6b6b"
175
- };
176
- var muted = "#8a8a93";
177
- var accent = "#ff6b00";
178
- var mono = "12px ui-monospace, SFMono-Regular, Menlo, monospace";
179
- var btn = "font:inherit;color:#ff6b00;background:transparent;border:1px solid #2e2e3c;border-radius:4px;padding:3px 8px;cursor:pointer";
180
- var card = "background:#0b0b0d;border:1px solid #232330;border-radius:4px";
181
- function row(label, value) {
182
- return k("div", { style: "display:flex;gap:8px;margin:2px 0" }, [
183
- k("span", { style: `color:${muted};min-width:96px` }, label),
184
- k("span", { style: "color:#ececef;word-break:break-word" }, value)
185
- ]);
186
- }
187
- function diffLines(diff) {
188
- return diff.split("\n").map((ln) => {
189
- let color = muted;
190
- if (ln.startsWith("@@")) color = "#5fb3e0";
191
- else if (ln.startsWith("+++") || ln.startsWith("---")) color = muted;
192
- else if (ln.startsWith("+")) color = "#5fd18a";
193
- else if (ln.startsWith("-")) color = "#ff7a7a";
194
- else color = "#bfbfc6";
195
- return k("div", { style: `color:${color};white-space:pre` }, ln || " ");
196
- });
197
- }
198
- function renderMessageBody(text) {
199
- const parts = [];
200
- const lines = text.split("\n");
201
- let buf = [];
202
- let inCode = false;
203
- let lang = "";
204
- const flush = () => {
205
- if (buf.length === 0) return;
206
- parts.push({ code: inCode, lang, text: buf.join("\n") });
207
- buf = [];
208
- };
209
- for (const line of lines) {
210
- const fence = /^```(\w*)\s*$/.exec(line);
211
- if (fence) {
212
- flush();
213
- inCode = !inCode;
214
- lang = inCode ? fence[1] ?? "" : "";
215
- continue;
216
- }
217
- buf.push(line);
218
- }
219
- flush();
220
- return parts.map(
221
- (p) => p.code ? k(
222
- "div",
223
- {
224
- style: `${card};padding:8px;margin:4px 0;font:${mono};color:#ececef;overflow-x:auto;white-space:pre`
225
- },
226
- p.text
227
- ) : k(
228
- "div",
229
- { style: "white-space:pre-wrap;word-break:break-word" },
230
- p.text
231
- )
232
- );
233
- }
234
- function diffBlock(changes) {
235
- return changes.map(
236
- (c) => k("div", { style: "margin:6px 0" }, [
237
- k(
238
- "div",
239
- { style: "color:#ececef;margin-bottom:2px" },
240
- `${c.file} (${c.bytes}B)`
241
- ),
242
- k(
243
- "div",
244
- {
245
- style: `overflow:auto;${card};padding:8px;max-height:200px;font-size:11px;line-height:1.45`
246
- },
247
- diffLines(c.diff)
248
- )
249
- ])
250
- );
251
- }
252
- function App(props) {
253
- const [state, setState] = d("idle");
254
- const [detail, setDetail] = d("");
255
- const [client, setClient] = d(null);
256
- const [bundle, setBundle] = d(null);
257
- const [resolved, setResolved] = d(null);
258
- const [busy, setBusy] = d(false);
259
- const [open, setOpen] = d(false);
260
- const [showCtx, setShowCtx] = d(false);
261
- const [showSettings, setShowSettings] = d(false);
262
- const [autoApply, setAutoApply] = d(false);
263
- const [captureSettings, setCaptureSettingsState] = d(
264
- getCaptureSettings()
265
- );
266
- const [displayMediaActive, setDisplayMediaActive] = d(false);
267
- const [displayMediaDenied, setDisplayMediaDenied] = d(false);
268
- const [agentReady, setAgentReady] = d(null);
269
- const [agentNote, setAgentNote] = d("");
270
- const [chatInput, setChatInput] = d("");
271
- const [messages, setMessages] = d([]);
272
- const [turnBusy, setTurnBusy] = d(false);
273
- const [thinking, setThinking] = d("");
274
- const [activity, setActivity] = d("");
275
- const [elapsed, setElapsed] = d(0);
276
- const [pulse, setPulse] = d(0);
277
- const [changes, setChanges] = d([]);
278
- const [changeTurnId, setChangeTurnId] = d("");
279
- const [picked, setPicked] = d({});
280
- const [rejectReason, setRejectReason] = d("");
281
- const [history, setHistory] = d([]);
282
- const [revisitId, setRevisitId] = d("");
283
- const [commitMsg, setCommitMsg] = d("");
284
- const [sessionNote, setSessionNote] = d("");
285
- const [lastSel, setLastSel] = d(null);
286
- const [externalCount, setExternalCount] = d(0);
287
- const [activeTurn, setActiveTurn] = d(null);
288
- const autoApplyRef = A(false);
289
- const changesRef = A([]);
290
- const activeTurnRef = A(null);
291
- const threadRef = A(null);
292
- const panelRef = A(null);
293
- autoApplyRef.current = autoApply;
294
- changesRef.current = changes;
295
- activeTurnRef.current = activeTurn;
296
- const appendAgent = (delta) => setMessages((ms) => {
297
- const last = ms[ms.length - 1];
298
- if (last && last.role === "agent") {
299
- return [...ms.slice(0, -1), { role: "agent", text: last.text + delta }];
300
- }
301
- return [...ms, { role: "agent", text: delta }];
302
- });
303
- const storageKey = `insitue:session:${typeof window !== "undefined" ? window.location.origin : "default"}`;
304
- const hydratedRef = A(false);
305
- y(() => {
306
- if (hydratedRef.current) return;
307
- hydratedRef.current = true;
308
- try {
309
- const raw = window.localStorage.getItem(storageKey);
310
- if (!raw) return;
311
- const saved = JSON.parse(raw);
312
- if (Array.isArray(saved.messages)) setMessages(saved.messages);
313
- if (Array.isArray(saved.history)) setHistory(saved.history);
314
- if (typeof saved.autoApply === "boolean") setAutoApply(saved.autoApply);
315
- if (typeof saved.open === "boolean") setOpen(saved.open);
316
- } catch {
317
- }
318
- }, [storageKey]);
319
- y(() => {
320
- if (!hydratedRef.current) return;
321
- try {
322
- window.localStorage.setItem(
323
- storageKey,
324
- JSON.stringify({ messages, history, autoApply, open })
325
- );
326
- } catch {
327
- }
328
- }, [messages, history, autoApply, open, storageKey]);
329
- y(() => {
330
- installRuntimeCollectors();
331
- const c = new CompanionClient(props.port, {
332
- onState: (s, d2) => {
333
- setState(s);
334
- if (d2 !== void 0) setDetail(d2);
335
- },
336
- onResolved: (_id, r) => setResolved(r),
337
- onAgentStatus: (s) => {
338
- setAgentReady(s.ready);
339
- setAgentNote(
340
- s.blockers.length ? `blocked: ${s.blockers.join("; ")}` : s.warnings.join(" \xB7 ") || `agent ready (${s.transport})`
341
- );
342
- },
343
- onAgentEvent: (e) => {
344
- if (e.t === "agent-text") {
345
- setThinking("");
346
- setActivity("");
347
- appendAgent(e.delta);
348
- } else if (e.t === "agent-thinking") {
349
- setThinking(e.note.slice(-160));
350
- } else if (e.t === "agent-activity") {
351
- setActivity(e.label);
352
- } else if (e.t === "agent-turn-complete") {
353
- setThinking("");
354
- setActivity("");
355
- setTurnBusy(false);
356
- } else if (e.t === "agent-error") {
357
- setThinking("");
358
- setActivity("");
359
- appendAgent(`
360
-
361
- [agent-error] ${e.message}`);
362
- setTurnBusy(false);
363
- }
364
- },
365
- onChangeset: (turnId, files) => {
366
- setChangeTurnId(turnId);
367
- setChanges(files);
368
- setPicked(Object.fromEntries(files.map((f) => [f.file, true])));
369
- setRejectReason("");
370
- if (autoApplyRef.current) {
371
- appendAgent(`
372
- [insitu] auto-apply on \u2014 writing without review`);
373
- c.sendDecision(turnId, "approve");
374
- }
375
- },
376
- onApplied: (turnId, files, ref) => {
377
- const at = activeTurnRef.current;
378
- const entry = {
379
- turnId,
380
- prompt: at && at.turnId === turnId ? at.prompt : "(applied change)",
381
- sel: at && at.turnId === turnId ? at.sel : null,
382
- files,
383
- checkpointRef: ref,
384
- diff: changesRef.current,
385
- status: "applied",
386
- note: "HMR reloading\u2026",
387
- postError: false
388
- };
389
- setHistory((hs) => [entry, ...hs]);
390
- setMessages((ms) => [
391
- ...ms,
392
- {
393
- role: "agent",
394
- text: `\u2713 applied ${files.length} file${files.length > 1 ? "s" : ""} \u2192 filed to Session (${ref})`
395
- }
396
- ]);
397
- setChanges([]);
398
- setChangeTurnId("");
399
- setActiveTurn(null);
400
- setSessionNote("");
401
- const base = runtimeErrorCount();
402
- let ticks = 0;
403
- const iv = setInterval(() => {
404
- ticks++;
405
- const threw = runtimeErrorCount() > base;
406
- if (threw || ticks >= 10) {
407
- setHistory(
408
- (hs) => hs.map(
409
- (en) => en.turnId === turnId ? {
410
- ...en,
411
- note: threw ? "\u26A0 host threw after HMR" : "HMR settled clean",
412
- postError: threw
413
- } : en
414
- )
415
- );
416
- clearInterval(iv);
417
- }
418
- }, 500);
419
- },
420
- onUndone: (turnId) => setHistory(
421
- (hs) => hs.map(
422
- (en) => en.turnId === turnId ? { ...en, status: "undone", note: "undone \u2014 HMR reverting" } : en
423
- )
424
- ),
425
- onSessionUndone: () => setHistory(
426
- (hs) => hs.map(
427
- (en) => en.status === "applied" ? { ...en, status: "undone" } : en
428
- )
429
- ),
430
- onSessionCommitted: (commit) => {
431
- setHistory(
432
- (hs) => hs.map(
433
- (en) => en.status === "applied" ? { ...en, status: "committed" } : en
434
- )
435
- );
436
- setSessionNote(`committed as ${commit} (local only \u2014 not pushed)`);
437
- },
438
- // #162: companion pushes this on every CLI/MCP subscriber
439
- // connect/disconnect. Drives the "→ claude in terminal"
440
- // badge and the Send-routing branch.
441
- onSubscribersAttached: (count) => setExternalCount(count)
442
- });
443
- setClient(c);
444
- void c.connect();
445
- return () => c.dispose();
446
- }, [props.port]);
447
- y(() => {
448
- if (!turnBusy) {
449
- setElapsed(0);
450
- setPulse(0);
451
- return;
452
- }
453
- const t0 = Date.now();
454
- const a = setInterval(
455
- () => setElapsed(Math.floor((Date.now() - t0) / 1e3)),
456
- 1e3
457
- );
458
- const b = setInterval(() => setPulse((p) => p + 1), 300);
459
- return () => {
460
- clearInterval(a);
461
- clearInterval(b);
462
- };
463
- }, [turnBusy]);
464
- y(() => {
465
- const el = threadRef.current;
466
- if (el) el.scrollTop = el.scrollHeight;
467
- }, [messages, changes, turnBusy, activity]);
468
- y(() => {
469
- const off1 = onDisplayMediaChange((active, reason) => {
470
- setDisplayMediaActive(active);
471
- if (reason === "denied") setDisplayMediaDenied(true);
472
- if (reason === "granted") setDisplayMediaDenied(false);
473
- });
474
- const off2 = onCaptureSettingsChange((s) => setCaptureSettingsState(s));
475
- return () => {
476
- off1();
477
- off2();
478
- };
479
- }, []);
480
- y(() => {
481
- const onKey = (ev) => {
482
- const meta = ev.metaKey || ev.ctrlKey;
483
- if (meta && ev.key === "k") {
484
- ev.preventDefault();
485
- setOpen(true);
486
- setTimeout(() => {
487
- const ta = panelRef.current?.querySelector(
488
- "textarea"
489
- );
490
- ta?.focus();
491
- }, 0);
492
- } else if (ev.key === "Escape" && open && !chatInput.trim()) {
493
- setOpen(false);
494
- }
495
- };
496
- window.addEventListener("keydown", onKey);
497
- return () => window.removeEventListener("keydown", onKey);
498
- }, [open, chatInput]);
499
- const captureSel = async (sel) => {
500
- setLastSel(sel);
501
- const b = await buildBundle(sel);
502
- setBundle(b);
503
- setResolved(null);
504
- setMessages([]);
505
- setChanges([]);
506
- setRevisitId("");
507
- setOpen(true);
508
- setShowCtx(false);
509
- client?.submitCapture(b);
510
- return b;
511
- };
512
- const pick = async (mode) => {
513
- if (!client || state !== "connected") return;
514
- setBusy(true);
515
- try {
516
- const sel = await beginPick(mode);
517
- if (sel) await captureSel(sel);
518
- } finally {
519
- setBusy(false);
520
- }
521
- };
522
- const sendChat = () => {
523
- if (!client || !chatInput.trim() || turnBusy) return;
524
- const text = chatInput.trim();
525
- if (text.startsWith("/")) {
526
- const [cmd, ...rest] = text.split(/\s+/);
527
- if (cmd === "/clear") {
528
- setMessages([]);
529
- setChatInput("");
530
- return;
531
- }
532
- if (cmd === "/undo") {
533
- const top = history.find((h) => h.status === "applied");
534
- if (top) client.sendUndo(top.turnId);
535
- setChatInput("");
536
- return;
537
- }
538
- if (cmd === "/commit") {
539
- const msg = rest.join(" ") || "Apply InSitue session changes";
540
- client.sendCommitSession(msg);
541
- setChatInput("");
542
- return;
543
- }
544
- }
545
- if (!bundle) return;
546
- const turnId = bundle.id;
547
- setMessages((ms) => [...ms, { role: "user", text }]);
548
- setChatInput("");
549
- if (externalCount > 0) {
550
- client.sendAsk(turnId, bundle.id, text);
551
- setMessages((ms) => [
552
- ...ms,
553
- { role: "agent", text: "\u2192 Sent to claude in your terminal." }
554
- ]);
555
- return;
556
- }
557
- setActiveTurn({ turnId, prompt: text, sel: lastSel });
558
- setChanges([]);
559
- setRevisitId("");
560
- setActivity("starting");
561
- setTurnBusy(true);
562
- client.sendTurn(turnId, bundle.id, text);
563
- };
564
- const continueFrom = async (e) => {
565
- if (!client || !e.sel || busy || turnBusy) return;
566
- setBusy(true);
567
- try {
568
- await captureSel(e.sel);
569
- setChatInput("");
570
- } finally {
571
- setBusy(false);
572
- }
573
- };
574
- const recaptureFix = async (e) => {
575
- if (!client || !e.sel || busy || turnBusy) return;
576
- setBusy(true);
577
- try {
578
- const b = await captureSel(e.sel);
579
- if (b.runtime.errors.length > 0) {
580
- const turnId = b.id;
581
- const text = "fix the runtime error from the previous change";
582
- setActiveTurn({ turnId, prompt: text, sel: e.sel });
583
- setMessages([{ role: "user", text }]);
584
- setActivity("starting");
585
- setTurnBusy(true);
586
- client.sendTurn(
587
- turnId,
588
- b.id,
589
- "Your previous edit was applied but the running app now reports the runtime errors shown in the context above. Diagnose the cause and propose a corrected edit."
590
- );
591
- } else {
592
- setMessages([
593
- { role: "agent", text: "[insitu] re-captured \u2014 no runtime errors" }
594
- ]);
595
- }
596
- } finally {
597
- setBusy(false);
598
- }
599
- };
600
- const approve = () => {
601
- const chosen = changes.map((c) => c.file).filter((f) => picked[f] !== false);
602
- client?.sendDecision(
603
- changeTurnId,
604
- "approve",
605
- chosen.length === changes.length ? void 0 : chosen
606
- );
607
- };
608
- const reject = () => {
609
- client?.sendDecision(
610
- changeTurnId,
611
- "reject",
612
- void 0,
613
- rejectReason.trim() || void 0
614
- );
615
- setChanges([]);
616
- setMessages((ms) => [
617
- ...ms,
618
- { role: "agent", text: "[insitu] changes rejected" }
619
- ]);
620
- };
621
- const t = bundle?.target;
622
- const targetSummary = t ? `${t.componentStack[0]?.name ?? t.selector.split(">").pop()?.trim() ?? "selection"} \xB7 ${t.confidence}` : "no selection";
623
- const appliedCount = history.filter((e) => e.status === "applied").length;
624
- const revisit = history.find((e) => e.turnId === revisitId) || null;
625
- const spin = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
626
- const pill = {
627
- position: "fixed",
628
- bottom: "16px",
629
- right: "16px",
630
- zIndex: 2147483e3,
631
- display: "flex",
632
- alignItems: "center",
633
- gap: "10px",
634
- padding: "8px 12px",
635
- font: mono,
636
- color: "#ececef",
637
- background: "rgba(15,15,18,0.94)",
638
- border: "1px solid #2e2e3c",
639
- borderRadius: "6px",
640
- boxShadow: "0 6px 24px rgba(0,0,0,0.45)"
641
- };
642
- const ctx = bundle && showCtx ? k("div", { style: "margin:8px 0;color:#bfbfc6" }, [
643
- row("confidence", t?.confidence ?? "\u2014"),
644
- row("selector", t?.selector ?? "\u2014"),
645
- row(
646
- "components",
647
- t?.componentStack.map((c) => c.name).join(" < ") || "\u2014"
648
- ),
649
- row("tailwind", bundle.tailwindClasses.join(" ") || "\u2014"),
650
- row("styles", `${Object.keys(bundle.computedStyles).length} props`),
651
- row(
652
- "runtime",
653
- `${bundle.runtime.console.length} log \xB7 ${bundle.runtime.network.length} net \xB7 ${bundle.runtime.errors.length} err`
654
- ),
655
- row(
656
- "screenshot",
657
- bundle.screenshot ? "captured" : bundle.screenshotUnavailable ? `unavailable \u2014 ${bundle.screenshotUnavailable}` : "\u2014"
658
- ),
659
- bundle.screenshot ? k("img", {
660
- src: bundle.screenshot.dataUrl,
661
- style: `max-width:100%;margin:8px 0;${card}`
662
- }) : null,
663
- resolved ? k(
664
- "pre",
665
- {
666
- style: `white-space:pre;overflow:auto;${card};padding:10px;margin:6px 0 0;color:#bfbfc6;max-height:160px`
667
- },
668
- `${resolved.file}:${resolved.line}
669
-
670
- ${resolved.snippet}`
671
- ) : null
672
- ]) : null;
673
- const workingRow = turnBusy && k(
674
- "div",
675
- {
676
- style: `display:flex;gap:8px;align-items:center;margin:6px 0 0;color:${accent}`
677
- },
678
- [
679
- k("span", {}, spin[pulse % spin.length]),
680
- k(
681
- "span",
682
- {
683
- style: `color:${muted};overflow:hidden;text-overflow:ellipsis;white-space:nowrap`
684
- },
685
- `${activity || "working"} \xB7 ${elapsed}s`
686
- ),
687
- k(
688
- "button",
689
- {
690
- style: `${btn};margin-left:auto`,
691
- onClick: () => {
692
- client?.cancelTurn("cur");
693
- setTurnBusy(false);
694
- }
695
- },
696
- "stop"
697
- )
698
- ]
699
- );
700
- const thread = k(
701
- "div",
702
- {
703
- ref: threadRef,
704
- style: `max-height:300px;overflow:auto;margin:6px 0;display:flex;flex-direction:column;gap:6px`
705
- },
706
- [
707
- ...messages.map(
708
- (m, i) => k(
709
- "div",
710
- {
711
- style: m.role === "user" ? `align-self:flex-end;max-width:88%;${card};border-color:#2e2e3c;padding:6px 8px;color:#ececef;white-space:pre-wrap;word-break:break-word;cursor:pointer` : `align-self:flex-start;max-width:96%;padding:6px 8px;color:#ececef`,
712
- // Click any prior user message to re-populate the
713
- // input — matches Claude.ai / Cursor edit-and-retry.
714
- // The original turn stays in the thread; sending
715
- // creates a new turn.
716
- onClick: m.role === "user" ? () => {
717
- setChatInput(m.text);
718
- setTimeout(() => {
719
- const ta = panelRef.current?.querySelector(
720
- "textarea"
721
- );
722
- ta?.focus();
723
- }, 0);
724
- } : void 0,
725
- title: m.role === "user" ? "click to edit + retry" : void 0
726
- },
727
- m.role === "agent" ? renderMessageBody(m.text) : m.text
728
- )
729
- ),
730
- thinking && turnBusy ? k(
731
- "div",
732
- {
733
- style: `align-self:flex-start;color:${muted};font-style:italic;white-space:pre-wrap;word-break:break-word`
734
- },
735
- `\u{1F4AD} ${thinking}`
736
- ) : null
737
- ]
738
- );
739
- const proposed = changes.length ? k("div", { style: "margin-top:8px" }, [
740
- k(
741
- "div",
742
- {
743
- style: `color:${accent};margin-bottom:4px;display:flex;justify-content:space-between;align-items:center`
744
- },
745
- [
746
- k(
747
- "span",
748
- {},
749
- `PROPOSED \xB7 ${changes.length} file${changes.length > 1 ? "s" : ""}`
750
- ),
751
- k(
752
- "span",
753
- { style: "display:flex;gap:6px;align-items:center" },
754
- [
755
- k(
756
- "button",
757
- { style: btn, onClick: approve },
758
- "Approve & write"
759
- ),
760
- k(
761
- "button",
762
- { style: `${btn};color:${muted}`, onClick: reject },
763
- "Reject"
764
- )
765
- ]
766
- )
767
- ]
768
- ),
769
- k("input", {
770
- value: rejectReason,
771
- placeholder: "reject reason (optional) \u2014 fed to the agent",
772
- onInput: (ev) => setRejectReason(ev.target.value),
773
- style: `width:100%;box-sizing:border-box;font:inherit;color:#ececef;${card};padding:5px 7px;margin:4px 0 6px`
774
- }),
775
- ...changes.map(
776
- (c) => k("div", { style: "margin:6px 0" }, [
777
- k(
778
- "label",
779
- {
780
- style: "color:#ececef;margin-bottom:2px;display:flex;gap:6px;align-items:center;cursor:pointer"
781
- },
782
- [
783
- k("input", {
784
- type: "checkbox",
785
- checked: picked[c.file] !== false,
786
- onChange: (ev) => setPicked((p) => ({
787
- ...p,
788
- [c.file]: ev.target.checked
789
- }))
790
- }),
791
- k("span", {}, `${c.file} (${c.bytes}B)`)
792
- ]
793
- ),
794
- k(
795
- "div",
796
- {
797
- style: `overflow:auto;${card};padding:8px;max-height:200px;font-size:11px;line-height:1.45`
798
- },
799
- diffLines(c.diff)
800
- )
801
- ])
802
- )
803
- ]) : null;
804
- const conversation = revisit ? k(
805
- "div",
806
- { style: `margin-top:8px;border-top:1px solid #232330;padding-top:8px` },
807
- [
808
- k(
809
- "div",
810
- {
811
- style: `color:${accent};margin-bottom:6px;display:flex;justify-content:space-between;align-items:center`
812
- },
813
- [
814
- k("span", {}, `REVISIT \xB7 ${revisit.prompt}`.slice(0, 52)),
815
- k(
816
- "button",
817
- { style: btn, onClick: () => setRevisitId("") },
818
- "close"
819
- )
820
- ]
821
- ),
822
- k(
823
- "div",
824
- { style: `color:${muted};margin-bottom:6px` },
825
- `${revisit.status} \xB7 ${revisit.files.join(", ")} \xB7 ${revisit.checkpointRef}`
826
- ),
827
- ...diffBlock(revisit.diff)
828
- ]
829
- ) : bundle ? k(
830
- "div",
831
- {
832
- style: `margin-top:8px;border-top:1px solid #232330;padding-top:8px`
833
- },
834
- [
835
- k(
836
- "div",
837
- {
838
- style: `color:${accent};margin-bottom:6px;display:flex;justify-content:space-between`
839
- },
840
- [
841
- k("span", {}, "ASK"),
842
- autoApply ? k("span", { style: `color:${accent}` }, "auto-apply ON") : null
843
- ]
844
- ),
845
- agentReady === false ? k(
846
- "div",
847
- { style: "color:#ff6b6b;margin-bottom:6px" },
848
- agentNote
849
- ) : agentNote && !messages.length ? k(
850
- "div",
851
- { style: `color:${muted};margin-bottom:6px` },
852
- agentNote
853
- ) : null,
854
- messages.length ? thread : null,
855
- workingRow,
856
- proposed,
857
- k("textarea", {
858
- value: chatInput,
859
- placeholder: messages.length ? "reply\u2026 (the agent remembers this thread) \xB7 /undo /clear /commit" : "what does this do? \xB7 make the padding bigger \xB7 fix this bug \xB7 \u2318K to focus",
860
- rows: 2,
861
- onInput: (ev) => setChatInput(ev.target.value),
862
- onKeyDown: (ev) => {
863
- if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter")
864
- sendChat();
865
- },
866
- style: `width:100%;box-sizing:border-box;font:inherit;color:#ececef;${card};padding:8px;margin-top:8px;resize:vertical`
867
- }),
868
- k(
869
- "div",
870
- { style: "margin-top:6px" },
871
- k(
872
- "button",
873
- {
874
- style: btn,
875
- disabled: turnBusy || !chatInput.trim() || agentReady === false,
876
- onClick: sendChat
877
- },
878
- turnBusy ? `\u2026working ${elapsed}s` : "Send (\u2318\u21B5)"
879
- )
880
- )
881
- ]
882
- ) : null;
883
- const timeline = history.length ? k(
884
- "div",
885
- { style: `margin-top:8px;border-top:1px solid #232330;padding-top:8px` },
886
- [
887
- k(
888
- "div",
889
- {
890
- style: `color:${accent};margin-bottom:6px;display:flex;justify-content:space-between;align-items:center`
891
- },
892
- [
893
- k("span", {}, `SESSION \xB7 ${appliedCount} live`),
894
- appliedCount > 0 ? k(
895
- "button",
896
- {
897
- style: `${btn};color:${muted}`,
898
- onClick: () => client?.sendUndoSession()
899
- },
900
- "Undo all"
901
- ) : null
902
- ]
903
- ),
904
- ...history.map((e) => {
905
- const glyph = e.status === "applied" ? "\u2713" : e.status === "committed" ? "\u25C6" : "\u293A";
906
- const dim = e.status !== "applied";
907
- return k(
908
- "div",
909
- {
910
- style: `${card};padding:6px 8px;margin:4px 0;${dim ? "opacity:0.6;" : ""}`
911
- },
912
- [
913
- k(
914
- "div",
915
- {
916
- style: "display:flex;justify-content:space-between;gap:8px;align-items:center"
917
- },
918
- [
919
- k(
920
- "span",
921
- {
922
- style: "color:#ececef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
923
- },
924
- `${glyph} ${e.prompt}`
925
- ),
926
- k("span", { style: "display:flex;gap:6px;flex:none" }, [
927
- k(
928
- "button",
929
- { style: btn, onClick: () => setRevisitId(e.turnId) },
930
- "Revisit"
931
- ),
932
- e.sel ? k(
933
- "button",
934
- { style: btn, onClick: () => void continueFrom(e) },
935
- "Continue"
936
- ) : null,
937
- e.status === "applied" ? k(
938
- "button",
939
- {
940
- style: btn,
941
- onClick: () => client?.sendUndo(e.turnId)
942
- },
943
- "Undo"
944
- ) : null
945
- ])
946
- ]
947
- ),
948
- k(
949
- "div",
950
- { style: `color:${muted};margin-top:3px` },
951
- `${e.files.join(", ")} \xB7 ${e.checkpointRef}`
952
- ),
953
- e.note ? k(
954
- "div",
955
- {
956
- style: `margin-top:3px;color:${e.postError ? "#ff7a7a" : "#5fd18a"};display:flex;justify-content:space-between;align-items:center`
957
- },
958
- [
959
- k("span", {}, e.note),
960
- e.postError && e.sel ? k(
961
- "button",
962
- {
963
- style: `${btn};color:#0f0f12;background:${accent};border-color:${accent}`,
964
- onClick: () => void recaptureFix(e)
965
- },
966
- "\u26A0 Re-capture & fix"
967
- ) : null
968
- ]
969
- ) : null
970
- ]
971
- );
972
- }),
973
- appliedCount > 0 ? k(
974
- "div",
975
- {
976
- style: "display:flex;gap:6px;align-items:center;margin-top:6px"
977
- },
978
- [
979
- k("input", {
980
- value: commitMsg,
981
- placeholder: "commit message (optional)",
982
- onInput: (ev) => setCommitMsg(ev.target.value),
983
- style: `flex:1;box-sizing:border-box;font:inherit;color:#ececef;${card};padding:5px 7px`
984
- }),
985
- k(
986
- "button",
987
- {
988
- style: btn,
989
- onClick: () => client?.sendCommitSession(
990
- commitMsg.trim() || void 0
991
- )
992
- },
993
- "Commit (local)"
994
- )
995
- ]
996
- ) : null,
997
- sessionNote ? k("div", { style: "color:#5fd18a;margin-top:6px" }, sessionNote) : null
998
- ]
999
- ) : null;
1000
- const settings = showSettings ? k("div", { style: `${card};padding:10px;margin:8px 0` }, [
1001
- k(
1002
- "label",
1003
- {
1004
- style: "display:flex;gap:8px;align-items:center;cursor:pointer;color:#ececef"
1005
- },
1006
- [
1007
- k("input", {
1008
- type: "checkbox",
1009
- checked: autoApply,
1010
- onChange: (ev) => setAutoApply(ev.target.checked)
1011
- }),
1012
- k("span", {}, "Auto-apply (skip review)")
1013
- ]
1014
- ),
1015
- k(
1016
- "div",
1017
- { style: `color:${muted};margin-top:4px` },
1018
- "Writes proposed changes immediately. Still checkpointed & undoable; no manual gate. Resets on reload."
1019
- ),
1020
- k(
1021
- "label",
1022
- {
1023
- style: "display:flex;gap:8px;align-items:center;cursor:pointer;color:#ececef;margin-top:10px"
1024
- },
1025
- [
1026
- k("input", {
1027
- type: "checkbox",
1028
- checked: captureSettings.alwaysPixelPerfect,
1029
- onChange: (ev) => setCaptureSettings({
1030
- alwaysPixelPerfect: ev.target.checked
1031
- })
1032
- }),
1033
- k("span", {}, "Always pixel-perfect screenshots")
1034
- ]
1035
- ),
1036
- k(
1037
- "div",
1038
- { style: `color:${muted};margin-top:4px` },
1039
- "Skips the silent rasterise path and always uses tab capture. One permission per session; every screenshot is OS-pixel accurate."
1040
- )
1041
- ]) : null;
1042
- const captureActivePill = displayMediaActive ? k(
1043
- "div",
1044
- {
1045
- style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#1a2a1f;border:1px solid #2f5040;color:#9fe7b8;font-size:11px"
1046
- },
1047
- [
1048
- k(
1049
- "span",
1050
- { style: "display:inline-flex;align-items:center;gap:6px" },
1051
- [
1052
- k("span", {
1053
- style: "width:8px;height:8px;border-radius:50%;background:#2fd16b;box-shadow:0 0 6px #2fd16b"
1054
- }),
1055
- k("span", {}, "Tab capture active")
1056
- ]
1057
- ),
1058
- k(
1059
- "button",
1060
- {
1061
- style: `${btn};margin-left:auto`,
1062
- onClick: () => stopDisplayMedia("user"),
1063
- title: "Stop sharing this tab"
1064
- },
1065
- "Stop"
1066
- )
1067
- ]
1068
- ) : null;
1069
- const captureDeniedNudge = displayMediaDenied && !displayMediaActive ? k(
1070
- "div",
1071
- {
1072
- style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#2a1f1a;border:1px solid #5a3a2a;color:#e8c69f;font-size:11px"
1073
- },
1074
- [
1075
- k(
1076
- "span",
1077
- { style: "flex:1" },
1078
- "Screenshot missed some cross-origin content. Grant tab capture for pixel-perfect captures."
1079
- ),
1080
- k(
1081
- "button",
1082
- {
1083
- style: btn,
1084
- onClick: () => {
1085
- void retryDisplayMedia();
1086
- }
1087
- },
1088
- "Enable"
1089
- )
1090
- ]
1091
- ) : null;
1092
- const panel = open ? k(
1093
- "div",
1094
- {
1095
- ref: panelRef,
1096
- style: {
1097
- position: "fixed",
1098
- bottom: "64px",
1099
- right: "16px",
1100
- width: "440px",
1101
- maxHeight: "82vh",
1102
- overflow: "auto",
1103
- zIndex: 2147483e3,
1104
- font: mono,
1105
- color: "#ececef",
1106
- background: "rgba(15,15,18,0.97)",
1107
- border: "1px solid #2e2e3c",
1108
- borderRadius: "8px",
1109
- padding: "12px 14px",
1110
- boxShadow: "0 10px 36px rgba(0,0,0,0.55)"
1111
- }
1112
- },
1113
- [
1114
- k(
1115
- "div",
1116
- {
1117
- style: "display:flex;justify-content:space-between;align-items:center;gap:8px"
1118
- },
1119
- [
1120
- k(
1121
- "span",
1122
- {
1123
- style: `color:${accent};overflow:hidden;text-overflow:ellipsis;white-space:nowrap`
1124
- },
1125
- targetSummary
1126
- ),
1127
- k("span", { style: "display:flex;gap:6px;flex:none" }, [
1128
- bundle ? k(
1129
- "button",
1130
- { style: btn, onClick: () => setShowCtx((v) => !v) },
1131
- showCtx ? "details \u25BE" : "details \u25B8"
1132
- ) : null,
1133
- k(
1134
- "button",
1135
- { style: btn, onClick: () => setShowSettings((v) => !v) },
1136
- "\u2699"
1137
- ),
1138
- k(
1139
- "button",
1140
- { style: btn, onClick: () => setOpen(false) },
1141
- "close"
1142
- )
1143
- ])
1144
- ]
1145
- ),
1146
- // #162: external-claude attached badge. Lives at the top of
1147
- // the panel body so the user always sees where their Send
1148
- // will go before they type. Hidden when count = 0.
1149
- externalCount > 0 ? k(
1150
- "div",
1151
- {
1152
- style: "display:flex;align-items:center;gap:8px;margin:8px 0;padding:6px 10px;border-radius:4px;background:#0f1f15;border:1px solid #1f4030;color:#9fe7b8;font-size:11px"
1153
- },
1154
- [
1155
- k("span", {
1156
- style: "width:8px;height:8px;border-radius:50%;background:#2fd16b;box-shadow:0 0 6px #2fd16b;flex:none"
1157
- }),
1158
- k(
1159
- "span",
1160
- {},
1161
- `\u2192 claude in terminal (${externalCount === 1 ? "1 session" : `${externalCount} sessions`})`
1162
- )
1163
- ]
1164
- ) : null,
1165
- settings,
1166
- captureActivePill,
1167
- captureDeniedNudge,
1168
- ctx,
1169
- conversation,
1170
- timeline,
1171
- !bundle && !history.length ? k(
1172
- "div",
1173
- { style: `color:${muted};margin-top:10px` },
1174
- "Pick an element to start."
1175
- ) : null
1176
- ]
1177
- ) : null;
1178
- return k("div", {}, [
1179
- panel,
1180
- k("div", { style: pill }, [
1181
- k("span", {
1182
- style: `width:8px;height:8px;border-radius:50%;background:${DOT[state]};display:inline-block`
1183
- }),
1184
- k("strong", { style: "letter-spacing:0.08em" }, "InSitue"),
1185
- k(
1186
- "span",
1187
- {
1188
- style: `color:${muted};max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap`
1189
- },
1190
- turnBusy ? `${spin[pulse % spin.length]} ${activity || "working"}` : detail || state
1191
- ),
1192
- k(
1193
- "button",
1194
- {
1195
- disabled: state !== "connected" || busy,
1196
- onClick: () => pick("element"),
1197
- style: btn
1198
- },
1199
- busy ? "\u2026" : "Select"
1200
- ),
1201
- k(
1202
- "button",
1203
- {
1204
- disabled: state !== "connected" || busy,
1205
- onClick: () => pick("rect"),
1206
- style: btn
1207
- },
1208
- "Rect"
1209
- ),
1210
- bundle || history.length ? k(
1211
- "button",
1212
- { onClick: () => setOpen((v) => !v), style: btn },
1213
- open ? "hide" : "panel"
1214
- ) : null
1215
- ])
1216
- ]);
1217
- }
1218
- function mountInSitue(opts = {}) {
1219
- setCaptureSettings({ disableLayer2: true });
1220
- const host = document.createElement("div");
1221
- host.id = "insitu-root";
1222
- host.setAttribute("data-insitu", "");
1223
- document.body.appendChild(host);
1224
- const shadow = host.attachShadow({ mode: "open" });
1225
- const mountPoint = document.createElement("div");
1226
- shadow.appendChild(mountPoint);
1227
- R(k(App, { port: opts.port ?? 5747 }), mountPoint);
1228
- return () => {
1229
- R(null, mountPoint);
1230
- host.remove();
1231
- };
1232
- }
1233
-
1234
- export {
1235
- mountInSitue
1236
- };