@lopecode/channel 0.1.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/claude-code-pairing-module.js +788 -0
- package/inject-module.js +131 -0
- package/lopecode-channel.ts +583 -0
- package/package.json +40 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tomlarkworthy/claude-code-pairing — Observable module
|
|
3
|
+
*
|
|
4
|
+
* Two-way communication channel between Lopecode notebooks and Claude Code.
|
|
5
|
+
* Paste this into a <script type="text/plain" id="@tomlarkworthy/claude-code-pairing">
|
|
6
|
+
* in any lopecode notebook HTML, and add "@tomlarkworthy/claude-code-pairing" to bootconf.json mains.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const _cc_config = function _cc_config(){return(
|
|
10
|
+
{ port: 8787, host: "127.0.0.1" }
|
|
11
|
+
)};
|
|
12
|
+
|
|
13
|
+
const _cc_notebook_id = function _cc_notebook_id(){return(
|
|
14
|
+
location.href
|
|
15
|
+
)};
|
|
16
|
+
|
|
17
|
+
const _cc_status = function _cc_status(Inputs){return(
|
|
18
|
+
Inputs.input("disconnected")
|
|
19
|
+
)};
|
|
20
|
+
|
|
21
|
+
const _cc_messages = function _cc_messages(Inputs){return(
|
|
22
|
+
Inputs.input([])
|
|
23
|
+
)};
|
|
24
|
+
|
|
25
|
+
const _cc_watches = function _cc_watches(Inputs){return(
|
|
26
|
+
Inputs.input([])
|
|
27
|
+
)};
|
|
28
|
+
|
|
29
|
+
const _cc_ws = function _cc_ws(cc_config, cc_notebook_id, cc_status, cc_messages, viewof_cc_watches, summarizeJS, observe, invalidation){return(
|
|
30
|
+
(function() {
|
|
31
|
+
var port = cc_config.port, host = cc_config.host;
|
|
32
|
+
var ws = null;
|
|
33
|
+
var paired = false;
|
|
34
|
+
|
|
35
|
+
function serializeValue(value, maxLen) {
|
|
36
|
+
maxLen = maxLen || 500;
|
|
37
|
+
try { return String(summarizeJS(value)).slice(0, maxLen); }
|
|
38
|
+
catch(e) { return String(value).slice(0, maxLen); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findModule(runtime, moduleName) {
|
|
42
|
+
if (!moduleName) return null; // no module filter — match any module
|
|
43
|
+
for (var v of runtime._variables) {
|
|
44
|
+
if (v._module && v._module._name === moduleName) return v._module;
|
|
45
|
+
if (v._name && v._name.startsWith("module ") && v._name === "module " + moduleName)
|
|
46
|
+
return v._module;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findActualRuntime(runtime) {
|
|
52
|
+
for (var v of runtime._variables) {
|
|
53
|
+
if (v._module && v._module._runtime && v._module._runtime._computeNow)
|
|
54
|
+
return v._module._runtime;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleCommand(cmd) {
|
|
60
|
+
var runtime = window.__ojs_runtime;
|
|
61
|
+
if (!runtime) return { ok: false, error: "Runtime not found" };
|
|
62
|
+
|
|
63
|
+
switch (cmd.action) {
|
|
64
|
+
case "get-variable": {
|
|
65
|
+
var name = cmd.params.name;
|
|
66
|
+
var moduleName = cmd.params.module;
|
|
67
|
+
var targetModule = findModule(runtime, moduleName);
|
|
68
|
+
for (var v of runtime._variables) {
|
|
69
|
+
if (v._name === name && (!targetModule || v._module === targetModule)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
result: {
|
|
73
|
+
name: v._name,
|
|
74
|
+
hasValue: v._value !== undefined,
|
|
75
|
+
hasError: v._error !== undefined,
|
|
76
|
+
value: serializeValue(v._value),
|
|
77
|
+
error: v._error ? v._error.message : undefined,
|
|
78
|
+
reachable: v._reachable
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { ok: false, error: "Variable not found: " + name };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "define-variable": {
|
|
87
|
+
var name = cmd.params.name;
|
|
88
|
+
var definition = cmd.params.definition;
|
|
89
|
+
var inputs = cmd.params.inputs || [];
|
|
90
|
+
var moduleName = cmd.params.module;
|
|
91
|
+
var targetModule = findModule(runtime, moduleName);
|
|
92
|
+
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
|
|
93
|
+
|
|
94
|
+
var fn;
|
|
95
|
+
try {
|
|
96
|
+
eval("fn = " + definition);
|
|
97
|
+
if (typeof fn !== "function") return { ok: false, error: "Definition must evaluate to a function" };
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return { ok: false, error: "Failed to parse definition: " + e.message };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var existingVar = null;
|
|
103
|
+
for (var v of runtime._variables) {
|
|
104
|
+
if (v._name === name && v._module === targetModule) { existingVar = v; break; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (existingVar) { existingVar.define(name, inputs, fn); }
|
|
109
|
+
else { var nv = targetModule.variable({}); nv.define(name, inputs, fn); }
|
|
110
|
+
var actualRuntime = findActualRuntime(runtime);
|
|
111
|
+
if (actualRuntime && actualRuntime._computeNow) actualRuntime._computeNow();
|
|
112
|
+
return { ok: true, result: { success: true, name: name, module: targetModule._name || "main", redefined: !!existingVar } };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { ok: false, error: "Failed to define variable: " + e.message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "delete-variable": {
|
|
119
|
+
var name = cmd.params.name;
|
|
120
|
+
var moduleName = cmd.params.module;
|
|
121
|
+
var targetModule = findModule(runtime, moduleName);
|
|
122
|
+
if (!targetModule) return { ok: false, error: "Module not found: " + (moduleName || "main") };
|
|
123
|
+
for (var v of runtime._variables) {
|
|
124
|
+
if (v._name === name && v._module === targetModule) {
|
|
125
|
+
v.delete();
|
|
126
|
+
return { ok: true, result: { success: true, name: name, module: targetModule._name || "main" } };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { ok: false, error: "Variable not found: " + name + " in module " + (moduleName || "main") };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case "list-variables": {
|
|
133
|
+
var moduleName = cmd.params.module;
|
|
134
|
+
var targetModule = findModule(runtime, moduleName);
|
|
135
|
+
var variables = [];
|
|
136
|
+
for (var v of runtime._variables) {
|
|
137
|
+
if (!v._name) continue;
|
|
138
|
+
if (targetModule && v._module !== targetModule) continue;
|
|
139
|
+
variables.push({
|
|
140
|
+
name: v._name,
|
|
141
|
+
module: (v._module && v._module._name) || "main",
|
|
142
|
+
hasValue: v._value !== undefined,
|
|
143
|
+
hasError: v._error !== undefined,
|
|
144
|
+
reachable: v._reachable
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
variables.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
148
|
+
return { ok: true, result: variables };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "run-tests": {
|
|
152
|
+
var filter = cmd.params.filter;
|
|
153
|
+
var timeout = cmd.params.timeout || 30000;
|
|
154
|
+
return runTests(runtime, filter, timeout);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "eval": {
|
|
158
|
+
var code = cmd.params.code;
|
|
159
|
+
try {
|
|
160
|
+
var result = eval(code);
|
|
161
|
+
return { ok: true, result: serializeValue(result) };
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return { ok: false, error: e.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "fork": {
|
|
168
|
+
return handleFork(runtime);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case "watch": {
|
|
172
|
+
return watchVariable(runtime, cmd.params.name, cmd.params.module);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "unwatch": {
|
|
176
|
+
return unwatchVariable(cmd.params.name, cmd.params.module);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "unwatch-all": {
|
|
180
|
+
return unwatchAll();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
return { ok: false, error: "Unknown action: " + cmd.action };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function runTests(runtime, filterStr, testTimeout) {
|
|
189
|
+
return new Promise(function(outerResolve) {
|
|
190
|
+
var actualRuntime = findActualRuntime(runtime);
|
|
191
|
+
if (!actualRuntime) { outerResolve({ ok: false, error: "Could not find actual runtime" }); return; }
|
|
192
|
+
|
|
193
|
+
var moduleNames = new Map();
|
|
194
|
+
for (var v of runtime._variables) {
|
|
195
|
+
if (v._module && !moduleNames.has(v._module)) {
|
|
196
|
+
var modName = v._module._name || (v._name && v._name.startsWith("module ") ? v._name : null);
|
|
197
|
+
if (modName) moduleNames.set(v._module, modName);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var testVars = [];
|
|
202
|
+
for (var variable of runtime._variables) {
|
|
203
|
+
var name = variable._name;
|
|
204
|
+
if (typeof name === "string" && name.startsWith("test_")) {
|
|
205
|
+
if (filterStr) {
|
|
206
|
+
var moduleName = moduleNames.get(variable._module) || "";
|
|
207
|
+
if (!name.includes(filterStr) && !moduleName.includes(filterStr)) continue;
|
|
208
|
+
}
|
|
209
|
+
testVars.push(variable);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (testVars.length === 0) { outerResolve({ ok: false, error: "No test variables found" }); return; }
|
|
214
|
+
|
|
215
|
+
var results = new Map();
|
|
216
|
+
var pendingPromises = [];
|
|
217
|
+
|
|
218
|
+
for (var vi = 0; vi < testVars.length; vi++) {
|
|
219
|
+
(function(v) {
|
|
220
|
+
var name = v._name;
|
|
221
|
+
var moduleName = moduleNames.get(v._module) || "main";
|
|
222
|
+
var fullName = moduleName + "#" + name;
|
|
223
|
+
|
|
224
|
+
var p = new Promise(function(resolve) {
|
|
225
|
+
var timeoutId = setTimeout(function() {
|
|
226
|
+
results.set(fullName, { state: "timeout", name: name, module: moduleName });
|
|
227
|
+
resolve();
|
|
228
|
+
}, testTimeout);
|
|
229
|
+
|
|
230
|
+
if (v._value !== undefined) {
|
|
231
|
+
clearTimeout(timeoutId);
|
|
232
|
+
results.set(fullName, { state: "passed", name: name, module: moduleName, value: String(v._value).slice(0, 200) });
|
|
233
|
+
resolve(); return;
|
|
234
|
+
}
|
|
235
|
+
if (v._error !== undefined) {
|
|
236
|
+
clearTimeout(timeoutId);
|
|
237
|
+
results.set(fullName, { state: "failed", name: name, module: moduleName, error: (v._error && v._error.message) || String(v._error) });
|
|
238
|
+
resolve(); return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!v._reachable) { v._reachable = true; actualRuntime._dirty.add(v); }
|
|
242
|
+
|
|
243
|
+
var oldObserver = v._observer;
|
|
244
|
+
v._observer = {
|
|
245
|
+
fulfilled: function(value) {
|
|
246
|
+
clearTimeout(timeoutId);
|
|
247
|
+
results.set(fullName, { state: "passed", name: name, module: moduleName, value: value === undefined ? "undefined" : String(value).slice(0, 200) });
|
|
248
|
+
resolve();
|
|
249
|
+
if (oldObserver && oldObserver.fulfilled) oldObserver.fulfilled(value);
|
|
250
|
+
},
|
|
251
|
+
rejected: function(error) {
|
|
252
|
+
clearTimeout(timeoutId);
|
|
253
|
+
results.set(fullName, { state: "failed", name: name, module: moduleName, error: (error && error.message) || String(error) });
|
|
254
|
+
resolve();
|
|
255
|
+
if (oldObserver && oldObserver.rejected) oldObserver.rejected(error);
|
|
256
|
+
},
|
|
257
|
+
pending: function() { if (oldObserver && oldObserver.pending) oldObserver.pending(); }
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
pendingPromises.push(p);
|
|
261
|
+
})(testVars[vi]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
actualRuntime._computeNow();
|
|
265
|
+
|
|
266
|
+
Promise.race([
|
|
267
|
+
Promise.all(pendingPromises),
|
|
268
|
+
new Promise(function(r) { setTimeout(r, testTimeout + 5000); })
|
|
269
|
+
]).then(function() {
|
|
270
|
+
var tests = Array.from(results.values());
|
|
271
|
+
var passed = tests.filter(function(t) { return t.state === "passed"; }).length;
|
|
272
|
+
var failed = tests.filter(function(t) { return t.state === "failed"; }).length;
|
|
273
|
+
var timeoutCount = tests.filter(function(t) { return t.state === "timeout"; }).length;
|
|
274
|
+
outerResolve({ ok: true, result: { tests: tests, summary: { total: tests.length, passed: passed, failed: failed, timeout: timeoutCount } } });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- Variable watching ---
|
|
280
|
+
var watchers = new Map(); // key: "module:name" -> { dispose: function }
|
|
281
|
+
|
|
282
|
+
function watchVariable(runtime, name, moduleName) {
|
|
283
|
+
var key = (moduleName || "main") + ":" + name;
|
|
284
|
+
if (watchers.has(key)) return { ok: true, result: { already_watching: true, key: key } };
|
|
285
|
+
|
|
286
|
+
var targetModule = findModule(runtime, moduleName);
|
|
287
|
+
var targetVar = null;
|
|
288
|
+
for (var v of runtime._variables) {
|
|
289
|
+
if (v._name === name && (!targetModule || v._module === targetModule)) {
|
|
290
|
+
targetVar = v;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!targetVar) return { ok: false, error: "Variable not found: " + name };
|
|
295
|
+
|
|
296
|
+
var moduleName = (targetVar._module && targetVar._module._name) || "main";
|
|
297
|
+
var debounceTimer = null;
|
|
298
|
+
var latestValue = undefined;
|
|
299
|
+
var latestError = undefined;
|
|
300
|
+
|
|
301
|
+
function flush() {
|
|
302
|
+
debounceTimer = null;
|
|
303
|
+
var serialized = latestError
|
|
304
|
+
? ("Error: " + (latestError.message || String(latestError)))
|
|
305
|
+
: serializeValue(latestValue, 2000);
|
|
306
|
+
var now = new Date().toLocaleTimeString();
|
|
307
|
+
|
|
308
|
+
// Update cc_watches table
|
|
309
|
+
var watches = viewof_cc_watches.value.slice();
|
|
310
|
+
var found = false;
|
|
311
|
+
for (var i = 0; i < watches.length; i++) {
|
|
312
|
+
if (watches[i].name === name && watches[i].module === moduleName) {
|
|
313
|
+
watches[i] = { name: name, module: moduleName, value: serialized.slice(0, 200), updated: now };
|
|
314
|
+
found = true;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!found) watches.push({ name: name, module: moduleName, value: serialized.slice(0, 200), updated: now });
|
|
319
|
+
viewof_cc_watches.value = watches;
|
|
320
|
+
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
321
|
+
|
|
322
|
+
// Send over WebSocket if connected
|
|
323
|
+
if (!paired || !ws) return;
|
|
324
|
+
var msg = { type: "variable-update", name: name, module: moduleName };
|
|
325
|
+
if (latestError) { msg.error = latestError.message || String(latestError); }
|
|
326
|
+
else { msg.value = serialized; }
|
|
327
|
+
ws.send(JSON.stringify(msg));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function scheduleUpdate(value, error) {
|
|
331
|
+
latestValue = value;
|
|
332
|
+
latestError = error;
|
|
333
|
+
if (!debounceTimer) {
|
|
334
|
+
// First update fires immediately
|
|
335
|
+
flush();
|
|
336
|
+
} else {
|
|
337
|
+
clearTimeout(debounceTimer);
|
|
338
|
+
}
|
|
339
|
+
debounceTimer = setTimeout(flush, 1000);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Use runtime-sdk observe() for clean subscription
|
|
343
|
+
var cancel = observe(targetVar, {
|
|
344
|
+
fulfilled: function(value) { scheduleUpdate(value, null); },
|
|
345
|
+
rejected: function(error) { scheduleUpdate(null, error); }
|
|
346
|
+
}, { invalidation: invalidation });
|
|
347
|
+
|
|
348
|
+
watchers.set(key, {
|
|
349
|
+
dispose: function() {
|
|
350
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
351
|
+
cancel();
|
|
352
|
+
// Remove from watches table
|
|
353
|
+
var watches = viewof_cc_watches.value.filter(function(w) {
|
|
354
|
+
return !(w.name === name && w.module === moduleName);
|
|
355
|
+
});
|
|
356
|
+
viewof_cc_watches.value = watches;
|
|
357
|
+
viewof_cc_watches.dispatchEvent(new Event("input"));
|
|
358
|
+
watchers.delete(key);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return { ok: true, result: { watching: true, key: key } };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function unwatchVariable(name, moduleName) {
|
|
366
|
+
var key = (moduleName || "main") + ":" + name;
|
|
367
|
+
var watcher = watchers.get(key);
|
|
368
|
+
if (!watcher) return { ok: false, error: "Not watching: " + key };
|
|
369
|
+
watcher.dispose();
|
|
370
|
+
return { ok: true, result: { unwatched: true, key: key } };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function unwatchAll() {
|
|
374
|
+
var keys = Array.from(watchers.keys());
|
|
375
|
+
for (var i = 0; i < keys.length; i++) {
|
|
376
|
+
watchers.get(keys[i]).dispose();
|
|
377
|
+
}
|
|
378
|
+
return { ok: true, result: { unwatched_all: true, count: keys.length } };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function handleFork(runtime) {
|
|
382
|
+
return new Promise(function(resolve) {
|
|
383
|
+
for (var v of runtime._variables) {
|
|
384
|
+
if (v._name === "_exportToHTML" && typeof v._value === "function") {
|
|
385
|
+
try {
|
|
386
|
+
Promise.resolve(v._value()).then(function(html) {
|
|
387
|
+
resolve({ ok: true, result: { html: html } });
|
|
388
|
+
}).catch(function(e) {
|
|
389
|
+
resolve({ ok: false, error: "Export failed: " + e.message });
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
} catch (e) {
|
|
393
|
+
resolve({ ok: false, error: "Export failed: " + e.message });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
resolve({ ok: false, error: "_exportToHTML not found. Does this notebook include @tomlarkworthy/exporter-2?" });
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function connect(token) {
|
|
403
|
+
if (ws) { ws.close(); ws = null; }
|
|
404
|
+
|
|
405
|
+
// Parse port from token format LOPE-PORT-XXXX
|
|
406
|
+
var connectPort = port;
|
|
407
|
+
if (token) {
|
|
408
|
+
var parts = token.match(/^LOPE-(\d+)-[A-Z0-9]+$/);
|
|
409
|
+
if (parts) connectPort = parseInt(parts[1], 10);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
cc_status.value = "connecting";
|
|
413
|
+
cc_status.dispatchEvent(new Event("input"));
|
|
414
|
+
|
|
415
|
+
ws = new WebSocket("ws://" + host + ":" + connectPort + "/ws");
|
|
416
|
+
|
|
417
|
+
ws.onopen = function() {
|
|
418
|
+
if (token) {
|
|
419
|
+
cc_status.value = "pairing";
|
|
420
|
+
cc_status.dispatchEvent(new Event("input"));
|
|
421
|
+
ws.send(JSON.stringify({
|
|
422
|
+
type: "pair",
|
|
423
|
+
token: token,
|
|
424
|
+
url: cc_notebook_id,
|
|
425
|
+
title: document.title || "Untitled"
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
ws.onmessage = function(event) {
|
|
431
|
+
var msg;
|
|
432
|
+
try { msg = JSON.parse(event.data); } catch(e) { return; }
|
|
433
|
+
|
|
434
|
+
switch (msg.type) {
|
|
435
|
+
case "paired":
|
|
436
|
+
paired = true;
|
|
437
|
+
cc_status.value = "connected";
|
|
438
|
+
cc_status.dispatchEvent(new Event("input"));
|
|
439
|
+
var modules = [];
|
|
440
|
+
var runtime = window.__ojs_runtime;
|
|
441
|
+
if (runtime) {
|
|
442
|
+
var seen = new Set();
|
|
443
|
+
for (var v of runtime._variables) {
|
|
444
|
+
var name = v._module && v._module._name;
|
|
445
|
+
if (name && !seen.has(name)) { seen.add(name); modules.push(name); }
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
ws.send(JSON.stringify({
|
|
449
|
+
type: "notebook-info",
|
|
450
|
+
url: cc_notebook_id,
|
|
451
|
+
title: document.title,
|
|
452
|
+
modules: modules,
|
|
453
|
+
hash: location.hash
|
|
454
|
+
}));
|
|
455
|
+
|
|
456
|
+
// Auto-watch default variables
|
|
457
|
+
var runtime2 = window.__ojs_runtime;
|
|
458
|
+
if (runtime2) {
|
|
459
|
+
watchVariable(runtime2, "hash", null);
|
|
460
|
+
watchVariable(runtime2, "currentModules", null);
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case "pair-failed":
|
|
465
|
+
paired = false;
|
|
466
|
+
cc_status.value = "disconnected";
|
|
467
|
+
cc_status.dispatchEvent(new Event("input"));
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case "reply":
|
|
471
|
+
var msgs = cc_messages.value.concat([{
|
|
472
|
+
role: "assistant",
|
|
473
|
+
content: msg.markdown,
|
|
474
|
+
timestamp: Date.now()
|
|
475
|
+
}]);
|
|
476
|
+
cc_messages.value = msgs;
|
|
477
|
+
cc_messages.dispatchEvent(new Event("input"));
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case "command":
|
|
481
|
+
Promise.resolve(handleCommand(msg)).then(function(result) {
|
|
482
|
+
ws.send(JSON.stringify({
|
|
483
|
+
type: "command-result",
|
|
484
|
+
id: msg.id,
|
|
485
|
+
ok: result.ok,
|
|
486
|
+
result: result.result,
|
|
487
|
+
error: result.error
|
|
488
|
+
}));
|
|
489
|
+
}).catch(function(e) {
|
|
490
|
+
ws.send(JSON.stringify({
|
|
491
|
+
type: "command-result",
|
|
492
|
+
id: msg.id,
|
|
493
|
+
ok: false,
|
|
494
|
+
error: e.message
|
|
495
|
+
}));
|
|
496
|
+
});
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
ws.onclose = function() {
|
|
502
|
+
paired = false;
|
|
503
|
+
cc_status.value = "disconnected";
|
|
504
|
+
cc_status.dispatchEvent(new Event("input"));
|
|
505
|
+
ws = null;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
ws.onerror = function() {};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Auto-connect if cc=TOKEN is in the hash fragment
|
|
512
|
+
// Hash format: #view=R100(...)&cc=LOPE-PORT-XXXX
|
|
513
|
+
(function autoConnect() {
|
|
514
|
+
var hash = location.hash || "";
|
|
515
|
+
var match = hash.match(/[&?]cc=(LOPE-[A-Z0-9-]+)/);
|
|
516
|
+
if (match) {
|
|
517
|
+
var token = match[1];
|
|
518
|
+
// Small delay to let the WebSocket server be ready
|
|
519
|
+
setTimeout(function() { connect(token); }, 500);
|
|
520
|
+
}
|
|
521
|
+
})();
|
|
522
|
+
|
|
523
|
+
invalidation.then(function() {
|
|
524
|
+
if (ws) { ws.close(); ws = null; }
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return { connect: connect, get paired() { return paired; }, get ws() { return ws; } };
|
|
528
|
+
})()
|
|
529
|
+
)};
|
|
530
|
+
|
|
531
|
+
const _cc_watch_table = function _cc_watch_table(cc_watches, Inputs){return(
|
|
532
|
+
Inputs.table(cc_watches, {
|
|
533
|
+
columns: ["name", "module", "value", "updated"],
|
|
534
|
+
header: { name: "Variable", module: "Module", value: "Value", updated: "Updated" },
|
|
535
|
+
width: { name: 120, module: 140, value: 300, updated: 80 }
|
|
536
|
+
})
|
|
537
|
+
)};
|
|
538
|
+
|
|
539
|
+
const _cc_change_forwarder = function _cc_change_forwarder(cc_ws, invalidation){return(
|
|
540
|
+
(function() {
|
|
541
|
+
var highWaterMark = 0;
|
|
542
|
+
var initializing = true;
|
|
543
|
+
|
|
544
|
+
var interval = setInterval(function() {
|
|
545
|
+
if (!cc_ws.paired || !cc_ws.ws) return;
|
|
546
|
+
var runtime = window.__ojs_runtime;
|
|
547
|
+
if (!runtime) return;
|
|
548
|
+
|
|
549
|
+
for (var v of runtime._variables) {
|
|
550
|
+
if (v._name === "history" && v._value && Array.isArray(v._value)) {
|
|
551
|
+
var history = v._value;
|
|
552
|
+
var total = history.length;
|
|
553
|
+
|
|
554
|
+
if (initializing) { highWaterMark = total; initializing = false; return; }
|
|
555
|
+
if (total <= highWaterMark) return;
|
|
556
|
+
|
|
557
|
+
var newEntries = history.slice(highWaterMark).map(function(e) {
|
|
558
|
+
return {
|
|
559
|
+
t: e.t, op: e.op, module: e.module, _name: e._name,
|
|
560
|
+
_inputs: e._inputs,
|
|
561
|
+
_definition: typeof e._definition === "function"
|
|
562
|
+
? e._definition.toString().slice(0, 500)
|
|
563
|
+
: String(e._definition || "").slice(0, 500)
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
highWaterMark = total;
|
|
567
|
+
cc_ws.ws.send(JSON.stringify({ type: "cell-change", changes: newEntries }));
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}, 1000);
|
|
572
|
+
|
|
573
|
+
invalidation.then(function() { clearInterval(interval); });
|
|
574
|
+
return "change forwarder active";
|
|
575
|
+
})()
|
|
576
|
+
)};
|
|
577
|
+
|
|
578
|
+
const _cc_chat = function _cc_chat(cc_messages, cc_status, cc_ws, md, htl, Inputs){return(
|
|
579
|
+
(function() {
|
|
580
|
+
var statusColors = {
|
|
581
|
+
connected: "#22c55e",
|
|
582
|
+
connecting: "#f59e0b",
|
|
583
|
+
pairing: "#f59e0b",
|
|
584
|
+
disconnected: "#ef4444"
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
function renderSetup() {
|
|
588
|
+
var tokenInput = document.createElement("input");
|
|
589
|
+
tokenInput.type = "text";
|
|
590
|
+
tokenInput.placeholder = "LOPE-XXXX";
|
|
591
|
+
tokenInput.style.cssText = "font-family:monospace;font-size:16px;padding:8px 12px;border:2px solid #d1d5db;border-radius:6px;width:140px;text-transform:uppercase;letter-spacing:2px;";
|
|
592
|
+
|
|
593
|
+
var connectBtn = document.createElement("button");
|
|
594
|
+
connectBtn.textContent = "Connect";
|
|
595
|
+
connectBtn.style.cssText = "padding:8px 20px;background:#2563eb;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:500;";
|
|
596
|
+
connectBtn.onclick = function() {
|
|
597
|
+
var token = tokenInput.value.trim();
|
|
598
|
+
if (token) cc_ws.connect(token);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
tokenInput.addEventListener("keydown", function(e) {
|
|
602
|
+
if (e.key === "Enter") connectBtn.click();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
var container = document.createElement("div");
|
|
606
|
+
container.className = "cc-setup";
|
|
607
|
+
container.innerHTML = '<div style="padding:24px;max-width:400px;margin:0 auto;text-align:center;">' +
|
|
608
|
+
'<div style="font-size:24px;margin-bottom:8px;">Claude Channel</div>' +
|
|
609
|
+
'<p style="color:#6b7280;margin-bottom:20px;font-size:14px;">Connect to Claude Code to chat with Claude from this notebook.</p>' +
|
|
610
|
+
'<div style="text-align:left;background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:20px;font-size:13px;">' +
|
|
611
|
+
'<div style="font-weight:600;margin-bottom:8px;">Setup:</div>' +
|
|
612
|
+
'<ol style="margin:0;padding-left:20px;line-height:1.8;">' +
|
|
613
|
+
'<li>Start Claude with the channel flag:<br><code style="background:#e5e7eb;padding:2px 6px;border-radius:3px;font-size:12px;">claude --dangerously-load-development-channels server:lopecode</code></li>' +
|
|
614
|
+
'<li>Copy the pairing token from the terminal</li>' +
|
|
615
|
+
'<li>Paste it below and click Connect</li>' +
|
|
616
|
+
'</ol></div>' +
|
|
617
|
+
'<div style="display:flex;gap:8px;justify-content:center;align-items:center;" class="cc-token-row"></div>' +
|
|
618
|
+
'</div>';
|
|
619
|
+
container.querySelector(".cc-token-row").append(tokenInput, connectBtn);
|
|
620
|
+
return container;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function renderChat() {
|
|
624
|
+
var messagesDiv = document.createElement("div");
|
|
625
|
+
messagesDiv.className = "cc-messages";
|
|
626
|
+
messagesDiv.style.cssText = "flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px;";
|
|
627
|
+
|
|
628
|
+
var textarea = document.createElement("textarea");
|
|
629
|
+
textarea.className = "cc-input";
|
|
630
|
+
textarea.placeholder = "Message Claude...";
|
|
631
|
+
textarea.rows = 2;
|
|
632
|
+
textarea.style.cssText = "width:100%;box-sizing:border-box;resize:none;border:1px solid #d1d5db;border-radius:8px;padding:10px 12px;font-family:inherit;font-size:14px;outline:none;";
|
|
633
|
+
|
|
634
|
+
var sendBtn = document.createElement("button");
|
|
635
|
+
sendBtn.textContent = "Send";
|
|
636
|
+
sendBtn.style.cssText = "padding:8px 16px;background:#2563eb;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;margin-left:auto;";
|
|
637
|
+
|
|
638
|
+
function sendMessage() {
|
|
639
|
+
var text = textarea.value.trim();
|
|
640
|
+
if (!text || !cc_ws.ws) return;
|
|
641
|
+
var msgs = cc_messages.value.concat([{ role: "user", content: text, timestamp: Date.now() }]);
|
|
642
|
+
cc_messages.value = msgs;
|
|
643
|
+
cc_messages.dispatchEvent(new Event("input"));
|
|
644
|
+
cc_ws.ws.send(JSON.stringify({ type: "message", content: text }));
|
|
645
|
+
textarea.value = "";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
sendBtn.onclick = sendMessage;
|
|
649
|
+
textarea.addEventListener("keydown", function(e) {
|
|
650
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
var inputRow = document.createElement("div");
|
|
654
|
+
inputRow.style.cssText = "display:flex;gap:8px;padding:12px;align-items:flex-end;border-top:1px solid #e5e7eb;";
|
|
655
|
+
inputRow.append(textarea, sendBtn);
|
|
656
|
+
|
|
657
|
+
var container = document.createElement("div");
|
|
658
|
+
container.className = "cc-chat-view";
|
|
659
|
+
container.style.cssText = "display:flex;flex-direction:column;height:100%;";
|
|
660
|
+
container.append(messagesDiv, inputRow);
|
|
661
|
+
|
|
662
|
+
function updateMessages() {
|
|
663
|
+
messagesDiv.innerHTML = "";
|
|
664
|
+
for (var i = 0; i < cc_messages.value.length; i++) {
|
|
665
|
+
var msg = cc_messages.value[i];
|
|
666
|
+
var bubble = document.createElement("div");
|
|
667
|
+
bubble.className = "cc-msg cc-msg-" + msg.role;
|
|
668
|
+
var isUser = msg.role === "user";
|
|
669
|
+
bubble.style.cssText = "max-width:80%;padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.5;" +
|
|
670
|
+
(isUser
|
|
671
|
+
? "align-self:flex-end;background:#2563eb;color:white;border-bottom-right-radius:4px;"
|
|
672
|
+
: "align-self:flex-start;background:#f3f4f6;color:#1f2937;border-bottom-left-radius:4px;");
|
|
673
|
+
|
|
674
|
+
if (isUser) {
|
|
675
|
+
bubble.textContent = msg.content;
|
|
676
|
+
} else {
|
|
677
|
+
try {
|
|
678
|
+
var rendered = md([msg.content]);
|
|
679
|
+
if (rendered instanceof Node) { bubble.appendChild(rendered); }
|
|
680
|
+
else { bubble.textContent = msg.content; }
|
|
681
|
+
} catch(e) { bubble.textContent = msg.content; }
|
|
682
|
+
}
|
|
683
|
+
messagesDiv.appendChild(bubble);
|
|
684
|
+
}
|
|
685
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
updateMessages();
|
|
689
|
+
container._updateMessages = updateMessages;
|
|
690
|
+
return container;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
var wrapper = document.createElement("div");
|
|
694
|
+
wrapper.className = "cc-chat";
|
|
695
|
+
wrapper.style.cssText = "border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;height:400px;display:flex;flex-direction:column;background:white;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;";
|
|
696
|
+
|
|
697
|
+
var statusBar = document.createElement("div");
|
|
698
|
+
statusBar.className = "cc-status-bar";
|
|
699
|
+
statusBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:8px 12px;border-bottom:1px solid #e5e7eb;font-size:12px;color:#6b7280;background:#fafafa;";
|
|
700
|
+
|
|
701
|
+
var statusDot = document.createElement("span");
|
|
702
|
+
statusDot.style.cssText = "width:8px;height:8px;border-radius:50%;display:inline-block;";
|
|
703
|
+
|
|
704
|
+
var statusText = document.createElement("span");
|
|
705
|
+
statusText.className = "cc-status-text";
|
|
706
|
+
|
|
707
|
+
statusBar.append(statusDot, statusText);
|
|
708
|
+
|
|
709
|
+
var body = document.createElement("div");
|
|
710
|
+
body.className = "cc-body";
|
|
711
|
+
body.style.cssText = "flex:1;overflow:hidden;display:flex;flex-direction:column;";
|
|
712
|
+
|
|
713
|
+
wrapper.append(statusBar, body);
|
|
714
|
+
|
|
715
|
+
var chatView = null;
|
|
716
|
+
|
|
717
|
+
function render() {
|
|
718
|
+
var status = cc_status.value || "disconnected";
|
|
719
|
+
statusDot.style.background = statusColors[status] || "#ef4444";
|
|
720
|
+
statusText.textContent = status === "connected" ? "Connected to Claude Code"
|
|
721
|
+
: (status === "connecting" || status === "pairing") ? "Connecting..."
|
|
722
|
+
: "Not connected";
|
|
723
|
+
|
|
724
|
+
body.innerHTML = "";
|
|
725
|
+
if (status === "connected") {
|
|
726
|
+
chatView = renderChat();
|
|
727
|
+
body.appendChild(chatView);
|
|
728
|
+
} else {
|
|
729
|
+
chatView = null;
|
|
730
|
+
body.appendChild(renderSetup());
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
render();
|
|
735
|
+
|
|
736
|
+
var lastStatus = cc_status.value;
|
|
737
|
+
var lastMsgCount = cc_messages.value.length;
|
|
738
|
+
var pollInterval = setInterval(function() {
|
|
739
|
+
if (cc_status.value !== lastStatus) { lastStatus = cc_status.value; render(); }
|
|
740
|
+
if (cc_messages.value.length !== lastMsgCount) {
|
|
741
|
+
lastMsgCount = cc_messages.value.length;
|
|
742
|
+
if (chatView && chatView._updateMessages) chatView._updateMessages();
|
|
743
|
+
}
|
|
744
|
+
}, 200);
|
|
745
|
+
|
|
746
|
+
var originalRemove = wrapper.remove.bind(wrapper);
|
|
747
|
+
wrapper.remove = function() { clearInterval(pollInterval); originalRemove(); };
|
|
748
|
+
|
|
749
|
+
return wrapper;
|
|
750
|
+
})()
|
|
751
|
+
)};
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
export default function define(runtime, observer) {
|
|
755
|
+
const main = runtime.module();
|
|
756
|
+
const $def = (pid, name, deps, fn) => {
|
|
757
|
+
main.variable(observer(name)).define(name, deps, fn).pid = pid;
|
|
758
|
+
};
|
|
759
|
+
const module_name = "@tomlarkworthy/claude-code-pairing";
|
|
760
|
+
|
|
761
|
+
// Visual cells first (controls render order)
|
|
762
|
+
$def("_cc_chat", "cc_chat", ["cc_messages","cc_status","cc_ws","md","htl","Inputs"], _cc_chat);
|
|
763
|
+
$def("_cc_watch_table", "cc_watch_table", ["cc_watches","Inputs"], _cc_watch_table);
|
|
764
|
+
|
|
765
|
+
// Internal cells
|
|
766
|
+
$def("_cc_config", "cc_config", [], _cc_config);
|
|
767
|
+
$def("_cc_notebook_id", "cc_notebook_id", [], _cc_notebook_id);
|
|
768
|
+
$def("_cc_status", "cc_status", ["Inputs"], _cc_status);
|
|
769
|
+
$def("_cc_messages", "cc_messages", ["Inputs"], _cc_messages);
|
|
770
|
+
$def("_cc_watches", "viewof cc_watches", ["Inputs"], _cc_watches);
|
|
771
|
+
main.variable().define("cc_watches", ["Generators", "viewof cc_watches"], (G, v) => G.input(v));
|
|
772
|
+
$def("_cc_ws", "cc_ws", ["cc_config","cc_notebook_id","cc_status","cc_messages","viewof cc_watches","summarizeJS","observe","invalidation"], _cc_ws);
|
|
773
|
+
$def("_cc_change_forwarder", "cc_change_forwarder", ["cc_ws","invalidation"], _cc_change_forwarder);
|
|
774
|
+
|
|
775
|
+
// Imports
|
|
776
|
+
main.define("module @tomlarkworthy/module-map", async () => runtime.module((await import("/@tomlarkworthy/module-map.js?v=4")).default));
|
|
777
|
+
main.define("currentModules", ["module @tomlarkworthy/module-map", "@variable"], (_, v) => v.import("currentModules", _));
|
|
778
|
+
main.define("moduleMap", ["module @tomlarkworthy/module-map", "@variable"], (_, v) => v.import("moduleMap", _));
|
|
779
|
+
main.define("module @tomlarkworthy/runtime-sdk", async () => runtime.module((await import("/@tomlarkworthy/runtime-sdk.js?v=4")).default));
|
|
780
|
+
main.define("viewof runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("viewof runtime_variables", _));
|
|
781
|
+
main.define("runtime_variables", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("runtime_variables", _));
|
|
782
|
+
main.define("observe", ["module @tomlarkworthy/runtime-sdk", "@variable"], (_, v) => v.import("observe", _));
|
|
783
|
+
main.define("module d/57d79353bac56631@44", async () => runtime.module((await import("/d/57d79353bac56631@44.js?v=4")).default));
|
|
784
|
+
main.define("hash", ["module d/57d79353bac56631@44", "@variable"], (_, v) => v.import("hash", _));
|
|
785
|
+
main.define("module @tomlarkworthy/summarizejs", async () => runtime.module((await import("/@tomlarkworthy/summarizejs.js?v=4")).default));
|
|
786
|
+
main.define("summarizeJS", ["module @tomlarkworthy/summarizejs", "@variable"], (_, v) => v.import("summarizeJS", _));
|
|
787
|
+
return main;
|
|
788
|
+
}
|