neural-loom 0.3.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/diagnostics/route-bundle-stats.json +1 -1
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page/react-loadable-manifest.json +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
- package/.next/server/middleware/middleware-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/middleware-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/{0uyuco-451j.n.js → 0qc.k.i3-z5fx.js} +1 -1
- package/.next/static/chunks/{14~-ojwsxf~oe.js → 0ru16isbveym7.js} +4 -4
- package/.next/trace +1 -1
- package/.next/trace-build +1 -1
- package/package.json +1 -1
- package/src/app/components/TerminalConsole.tsx +98 -38
- package/src/lib/agents/SessionPersister.ts +24 -8
- /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 2ep0r9tVD7fxPpgavvvkt}/_buildManifest.js +0 -0
- /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 2ep0r9tVD7fxPpgavvvkt}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 2ep0r9tVD7fxPpgavvvkt}/_ssgManifest.js +0 -0
|
@@ -19,22 +19,39 @@ interface LogBufferItem {
|
|
|
19
19
|
message?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
interface CopyResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
method?: "clipboard-api" | "execCommand";
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
+
async function copyTextToClipboard(text: string): Promise<CopyResult> {
|
|
29
|
+
if (typeof window === "undefined") return { ok: false, error: "no window" };
|
|
30
|
+
if (!text) return { ok: false, error: "empty selection" };
|
|
31
|
+
|
|
32
|
+
// Secure context (localhost, PWA, https): trust ONLY the async Clipboard API.
|
|
33
|
+
// It is reliable when it resolves — and we deliberately don't fall back to
|
|
34
|
+
// execCommand here, because execCommand can return a false-positive `true`
|
|
35
|
+
// (reporting success while copying nothing). Surfacing the real rejection is
|
|
36
|
+
// more useful than a fake "Copied".
|
|
37
|
+
if (window.isSecureContext) {
|
|
38
|
+
if (!navigator.clipboard?.writeText) {
|
|
39
|
+
return { ok: false, error: "Clipboard API unavailable in this browser" };
|
|
40
|
+
}
|
|
28
41
|
try {
|
|
29
42
|
await navigator.clipboard.writeText(text);
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
|
|
43
|
+
return { ok: true, method: "clipboard-api" };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
method: "clipboard-api",
|
|
48
|
+
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
|
|
49
|
+
};
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
53
|
+
// Non-secure context (e.g. plain-HTTP LAN access): execCommand is the only
|
|
54
|
+
// option. Note its success value is not fully reliable.
|
|
38
55
|
try {
|
|
39
56
|
const textArea = document.createElement("textarea");
|
|
40
57
|
textArea.value = text;
|
|
@@ -47,10 +64,11 @@ async function copyTextToClipboard(text: string): Promise<boolean> {
|
|
|
47
64
|
textArea.select();
|
|
48
65
|
const successful = document.execCommand("copy");
|
|
49
66
|
document.body.removeChild(textArea);
|
|
50
|
-
return
|
|
67
|
+
if (successful) return { ok: true, method: "execCommand" };
|
|
68
|
+
return { ok: false, method: "execCommand", error: "execCommand returned false (use http://localhost for the Clipboard API)" };
|
|
51
69
|
} catch (err) {
|
|
52
|
-
|
|
53
|
-
return false;
|
|
70
|
+
const ex = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return { ok: false, method: "execCommand", error: ex };
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
@@ -72,21 +90,40 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
72
90
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
|
73
91
|
const searchAddonRef = useRef<SearchAddon | null>(null);
|
|
74
92
|
|
|
75
|
-
// Copy feedback ("Copied!" indicator)
|
|
93
|
+
// Copy feedback ("Copied!" indicator) and last error (surfaced for diagnosis)
|
|
76
94
|
const [copied, setCopied] = useState(false);
|
|
95
|
+
const [copyError, setCopyError] = useState<string | null>(null);
|
|
77
96
|
const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
97
|
+
// Last non-empty terminal selection. The toolbar/right-click read this because
|
|
98
|
+
// clicking a button/menu clears the live xterm selection before the handler runs.
|
|
99
|
+
const lastSelectionRef = useRef("");
|
|
78
100
|
|
|
79
|
-
// Copy the given text (or the current terminal selection) to the clipboard.
|
|
101
|
+
// Copy the given text (or the last/current terminal selection) to the clipboard.
|
|
102
|
+
// IMPORTANT: must be invoked synchronously inside a user gesture (mouseup, click,
|
|
103
|
+
// keydown) so the Clipboard API write is permitted — otherwise it silently fails.
|
|
80
104
|
// Stable identity so the terminal effect can use it without re-initializing.
|
|
81
|
-
|
|
105
|
+
// `explicit` = triggered by a button/shortcut (surface "nothing selected").
|
|
106
|
+
const copySelection = useCallback((text?: string, explicit = false): void => {
|
|
82
107
|
const term = xtermRef.current;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
const live = term?.getSelection() ?? "";
|
|
109
|
+
const selection = (text ?? (live || lastSelectionRef.current) ?? "").trim();
|
|
110
|
+
if (!selection) {
|
|
111
|
+
if (explicit) {
|
|
112
|
+
setCopyError("Nothing selected to copy");
|
|
113
|
+
setTimeout(() => setCopyError(null), 2500);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
copyTextToClipboard(selection).then((result) => {
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
setCopyError(null);
|
|
120
|
+
setCopied(true);
|
|
121
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
122
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
|
|
123
|
+
} else {
|
|
124
|
+
console.error("[TerminalConsole] copy failed:", result.error);
|
|
125
|
+
setCopyError(result.error || "copy failed");
|
|
126
|
+
}
|
|
90
127
|
});
|
|
91
128
|
}, []);
|
|
92
129
|
|
|
@@ -220,31 +257,39 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
220
257
|
const handleMouseDown = (e: MouseEvent) => {
|
|
221
258
|
if (e.button === 2 && term.hasSelection()) e.stopPropagation();
|
|
222
259
|
};
|
|
260
|
+
// Copy-on-select. This runs on the bubble phase AFTER xterm has finalized the
|
|
261
|
+
// selection, and — crucially — synchronously inside the mouseup user gesture,
|
|
262
|
+
// so the Clipboard API write is allowed (the previous setTimeout ran outside
|
|
263
|
+
// the gesture and silently failed). Left-button only.
|
|
223
264
|
const handleMouseUp = (e: MouseEvent) => {
|
|
224
|
-
if (e.button === 2 && term.hasSelection())
|
|
265
|
+
if (e.button === 2 && term.hasSelection()) {
|
|
266
|
+
e.stopPropagation();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (e.button === 0) {
|
|
270
|
+
const sel = term.getSelection();
|
|
271
|
+
if (sel && sel.trim()) copySelection(sel);
|
|
272
|
+
}
|
|
225
273
|
};
|
|
226
274
|
const handleContextMenu = (e: MouseEvent) => {
|
|
227
|
-
|
|
275
|
+
const sel = term.getSelection() || lastSelectionRef.current;
|
|
276
|
+
if (sel && sel.trim()) {
|
|
228
277
|
e.preventDefault();
|
|
229
|
-
copySelection(
|
|
230
|
-
term.clearSelection();
|
|
278
|
+
copySelection(sel);
|
|
231
279
|
}
|
|
232
280
|
};
|
|
233
281
|
if (container) {
|
|
234
282
|
container.addEventListener("mousedown", handleMouseDown, true);
|
|
235
|
-
|
|
283
|
+
// bubble phase: xterm has already committed the selection by now
|
|
284
|
+
container.addEventListener("mouseup", handleMouseUp);
|
|
236
285
|
container.addEventListener("contextmenu", handleContextMenu);
|
|
237
286
|
}
|
|
238
287
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// immediately (debounced) rather than waiting for a later Ctrl+C/right-click.
|
|
242
|
-
let selTimer: ReturnType<typeof setTimeout> | null = null;
|
|
288
|
+
// Track the latest non-empty selection so button / shortcut / right-click can
|
|
289
|
+
// copy it even after a click or redraw clears the live xterm selection.
|
|
243
290
|
const selDisposable = term.onSelectionChange(() => {
|
|
244
291
|
const sel = term.getSelection();
|
|
245
|
-
if (
|
|
246
|
-
if (selTimer) clearTimeout(selTimer);
|
|
247
|
-
selTimer = setTimeout(() => copySelection(sel), 150);
|
|
292
|
+
if (sel && sel.trim()) lastSelectionRef.current = sel;
|
|
248
293
|
});
|
|
249
294
|
|
|
250
295
|
// Attach search key handler and clipboard copy handler
|
|
@@ -451,13 +496,12 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
451
496
|
return () => {
|
|
452
497
|
isDisposed = true;
|
|
453
498
|
clearTimeout(fitTimeout);
|
|
454
|
-
if (selTimer) clearTimeout(selTimer);
|
|
455
499
|
selDisposable.dispose();
|
|
456
500
|
resizeObserver.disconnect();
|
|
457
501
|
ws.close();
|
|
458
502
|
if (container) {
|
|
459
503
|
container.removeEventListener("mousedown", handleMouseDown, true);
|
|
460
|
-
container.removeEventListener("mouseup", handleMouseUp
|
|
504
|
+
container.removeEventListener("mouseup", handleMouseUp);
|
|
461
505
|
container.removeEventListener("contextmenu", handleContextMenu);
|
|
462
506
|
}
|
|
463
507
|
try {
|
|
@@ -578,8 +622,24 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
|
|
|
578
622
|
✓ Copied
|
|
579
623
|
</span>
|
|
580
624
|
)}
|
|
625
|
+
{copyError && (
|
|
626
|
+
<span
|
|
627
|
+
title={copyError}
|
|
628
|
+
style={{
|
|
629
|
+
color: "#ff6b6b",
|
|
630
|
+
fontSize: "0.65rem",
|
|
631
|
+
fontWeight: 600,
|
|
632
|
+
maxWidth: "260px",
|
|
633
|
+
overflow: "hidden",
|
|
634
|
+
textOverflow: "ellipsis",
|
|
635
|
+
whiteSpace: "nowrap",
|
|
636
|
+
}}
|
|
637
|
+
>
|
|
638
|
+
⚠ Copy failed: {copyError}
|
|
639
|
+
</span>
|
|
640
|
+
)}
|
|
581
641
|
<button
|
|
582
|
-
onClick={() => copySelection()}
|
|
642
|
+
onClick={() => copySelection(undefined, true)}
|
|
583
643
|
style={{
|
|
584
644
|
padding: "2px 6px",
|
|
585
645
|
borderRadius: "3px",
|
|
@@ -75,20 +75,36 @@ export class SessionPersister {
|
|
|
75
75
|
try {
|
|
76
76
|
const dir = this.getSessionsDir();
|
|
77
77
|
const filePath = path.join(dir, `${id}.json`);
|
|
78
|
+
const json = JSON.stringify(data, null, 2);
|
|
78
79
|
// Write to a temp file then rename, so a crash mid-write can't leave a
|
|
79
80
|
// truncated/corrupt session file (rename is atomic on the same filesystem).
|
|
80
81
|
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
81
|
-
fs.writeFile(tmpPath,
|
|
82
|
+
fs.writeFile(tmpPath, json, "utf8", (err) => {
|
|
82
83
|
if (err) {
|
|
83
|
-
console.error(`[SessionPersister] Failed to save session ${id}
|
|
84
|
+
console.error(`[SessionPersister] Failed to save session ${id}:`, err);
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// On Windows the rename can transiently fail with EPERM/EACCES/EBUSY when
|
|
88
|
+
// the destination is momentarily locked (AV scanners, the search indexer,
|
|
89
|
+
// or another open handle). Retry briefly, then fall back to overwriting
|
|
90
|
+
// the destination directly so the save still lands.
|
|
91
|
+
const tryRename = (attempts: number) => {
|
|
92
|
+
fs.rename(tmpPath, filePath, (renameErr) => {
|
|
93
|
+
if (!renameErr) return;
|
|
94
|
+
const code = (renameErr as NodeJS.ErrnoException).code;
|
|
95
|
+
if ((code === "EPERM" || code === "EACCES" || code === "EBUSY") && attempts > 0) {
|
|
96
|
+
setTimeout(() => tryRename(attempts - 1), 60);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
fs.writeFile(filePath, json, "utf8", (writeErr) => {
|
|
100
|
+
if (writeErr) {
|
|
101
|
+
console.error(`[SessionPersister] Failed to save session ${id}:`, writeErr);
|
|
102
|
+
}
|
|
103
|
+
fs.unlink(tmpPath, () => {});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
tryRename(5);
|
|
92
108
|
});
|
|
93
109
|
} catch (err) {
|
|
94
110
|
console.error(`[SessionPersister] Failed to save session ${id}:`, err);
|
|
File without changes
|
/package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 2ep0r9tVD7fxPpgavvvkt}/_clientMiddlewareManifest.js
RENAMED
|
File without changes
|
|
File without changes
|