neural-loom 0.3.0 → 0.3.1

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.
Files changed (48) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/diagnostics/route-bundle-stats.json +1 -1
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found.html +1 -1
  14. package/.next/server/app/_not-found.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/server/app/index.html +1 -1
  22. package/.next/server/app/index.rsc +2 -2
  23. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  24. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  25. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  27. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  28. package/.next/server/app/page/react-loadable-manifest.json +2 -2
  29. package/.next/server/app/page_client-reference-manifest.js +1 -1
  30. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js +1 -1
  31. package/.next/server/chunks/[root-of-the-server]__0li1h1u._.js.map +1 -1
  32. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js +1 -1
  33. package/.next/server/chunks/[root-of-the-server]__0tq0maq._.js.map +1 -1
  34. package/.next/server/middleware/middleware-manifest.json +1 -1
  35. package/.next/server/middleware-build-manifest.js +3 -3
  36. package/.next/server/middleware-manifest.json +1 -1
  37. package/.next/server/pages/404.html +1 -1
  38. package/.next/server/pages/500.html +1 -1
  39. package/.next/static/chunks/{14~-ojwsxf~oe.js → 0jboepcsqzdio.js} +4 -4
  40. package/.next/static/chunks/{0uyuco-451j.n.js → 125kj92.ndij4.js} +1 -1
  41. package/.next/trace +1 -1
  42. package/.next/trace-build +1 -1
  43. package/package.json +1 -1
  44. package/src/app/components/TerminalConsole.tsx +60 -19
  45. package/src/lib/agents/SessionPersister.ts +24 -8
  46. /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 3wzjx3BGzCrq3vZdFE3N-}/_buildManifest.js +0 -0
  47. /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 3wzjx3BGzCrq3vZdFE3N-}/_clientMiddlewareManifest.js +0 -0
  48. /package/.next/static/{04j4ht7eCOOXSLQ5Wixq0 → 3wzjx3BGzCrq3vZdFE3N-}/_ssgManifest.js +0 -0
@@ -19,22 +19,33 @@ interface LogBufferItem {
19
19
  message?: string;
20
20
  }
21
21
 
22
- async function copyTextToClipboard(text: string): Promise<boolean> {
23
- if (typeof window === "undefined" || !text) return false;
22
+ interface CopyResult {
23
+ ok: boolean;
24
+ method?: "clipboard-api" | "execCommand";
25
+ error?: string;
26
+ }
27
+
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
+ let apiError = "";
24
33
 
25
34
  // Preferred path: async Clipboard API. Reliable in a secure context, which
26
35
  // includes localhost (NeuralLoom's default bind).
27
36
  if (window.isSecureContext && navigator.clipboard?.writeText) {
28
37
  try {
29
38
  await navigator.clipboard.writeText(text);
30
- return true;
31
- } catch {
32
- // Fall through to the execCommand fallback (e.g. permission denied).
39
+ return { ok: true, method: "clipboard-api" };
40
+ } catch (err) {
41
+ // Fall through to the execCommand fallback (e.g. not focused / denied).
42
+ apiError = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
33
43
  }
44
+ } else if (!window.isSecureContext) {
45
+ apiError = "not a secure context (access via http://localhost for the Clipboard API)";
34
46
  }
35
47
 
36
- // Fallback: hidden textarea + execCommand("copy") for non-secure contexts
37
- // (e.g. accessed over a plain-HTTP LAN address).
48
+ // Fallback: hidden textarea + execCommand("copy").
38
49
  try {
39
50
  const textArea = document.createElement("textarea");
40
51
  textArea.value = text;
@@ -47,10 +58,11 @@ async function copyTextToClipboard(text: string): Promise<boolean> {
47
58
  textArea.select();
48
59
  const successful = document.execCommand("copy");
49
60
  document.body.removeChild(textArea);
50
- return successful;
61
+ if (successful) return { ok: true, method: "execCommand" };
62
+ return { ok: false, error: `execCommand returned false${apiError ? ` (clipboard API: ${apiError})` : ""}` };
51
63
  } catch (err) {
52
- console.warn("Clipboard copy failed:", err);
53
- return false;
64
+ const ex = err instanceof Error ? err.message : String(err);
65
+ return { ok: false, error: `${ex}${apiError ? ` | clipboard API: ${apiError}` : ""}` };
54
66
  }
55
67
  }
56
68
 
@@ -72,21 +84,34 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
72
84
  const [caseSensitive, setCaseSensitive] = useState(false);
73
85
  const searchAddonRef = useRef<SearchAddon | null>(null);
74
86
 
75
- // Copy feedback ("Copied!" indicator)
87
+ // Copy feedback ("Copied!" indicator) and last error (surfaced for diagnosis)
76
88
  const [copied, setCopied] = useState(false);
89
+ const [copyError, setCopyError] = useState<string | null>(null);
77
90
  const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
78
91
 
79
92
  // Copy the given text (or the current terminal selection) to the clipboard.
80
93
  // Stable identity so the terminal effect can use it without re-initializing.
81
- const copySelection = useCallback((text?: string): void => {
94
+ // `explicit` = triggered by a button/shortcut (surface "nothing selected").
95
+ const copySelection = useCallback((text?: string, explicit = false): void => {
82
96
  const term = xtermRef.current;
83
97
  const selection = (text ?? term?.getSelection() ?? "").trim();
84
- if (!selection) return;
85
- copyTextToClipboard(selection).then((ok) => {
86
- if (!ok) return;
87
- setCopied(true);
88
- if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
89
- copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
98
+ if (!selection) {
99
+ if (explicit) {
100
+ setCopyError("Nothing selected to copy");
101
+ setTimeout(() => setCopyError(null), 2500);
102
+ }
103
+ return;
104
+ }
105
+ copyTextToClipboard(selection).then((result) => {
106
+ if (result.ok) {
107
+ setCopyError(null);
108
+ setCopied(true);
109
+ if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
110
+ copiedTimerRef.current = setTimeout(() => setCopied(false), 1200);
111
+ } else {
112
+ console.error("[TerminalConsole] copy failed:", result.error);
113
+ setCopyError(result.error || "copy failed");
114
+ }
90
115
  });
91
116
  }, []);
92
117
 
@@ -578,8 +603,24 @@ export default function TerminalConsole({ sessionId, height = "400px", wsPort =
578
603
  ✓ Copied
579
604
  </span>
580
605
  )}
606
+ {copyError && (
607
+ <span
608
+ title={copyError}
609
+ style={{
610
+ color: "#ff6b6b",
611
+ fontSize: "0.65rem",
612
+ fontWeight: 600,
613
+ maxWidth: "260px",
614
+ overflow: "hidden",
615
+ textOverflow: "ellipsis",
616
+ whiteSpace: "nowrap",
617
+ }}
618
+ >
619
+ ⚠ Copy failed: {copyError}
620
+ </span>
621
+ )}
581
622
  <button
582
- onClick={() => copySelection()}
623
+ onClick={() => copySelection(undefined, true)}
583
624
  style={{
584
625
  padding: "2px 6px",
585
626
  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, JSON.stringify(data, null, 2), "utf8", (err) => {
82
+ fs.writeFile(tmpPath, json, "utf8", (err) => {
82
83
  if (err) {
83
- console.error(`[SessionPersister] Failed to save session ${id} asynchronously:`, err);
84
+ console.error(`[SessionPersister] Failed to save session ${id}:`, err);
84
85
  return;
85
86
  }
86
- fs.rename(tmpPath, filePath, (renameErr) => {
87
- if (renameErr) {
88
- console.error(`[SessionPersister] Failed to finalize session ${id}:`, renameErr);
89
- fs.unlink(tmpPath, () => {});
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);