@aion0/forge 0.10.51 → 0.10.53

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/RELEASE_NOTES.md CHANGED
@@ -1,8 +1,8 @@
1
- # Forge v0.10.51
1
+ # Forge v0.10.53
2
2
 
3
3
  Released: 2026-06-09
4
4
 
5
- ## Changes since v0.10.50
5
+ ## Changes since v0.10.52
6
6
 
7
7
 
8
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.50...v0.10.51
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.52...v0.10.53
@@ -0,0 +1,24 @@
1
+ /**
2
+ * /scratch/<path...> — in-browser viewer for files under <dataDir>/scratch/.
3
+ *
4
+ * Why this page exists: the raw `/api/scratch/<path>` route returns the
5
+ * file with its proper MIME, which works for images / pdf but is poor UX
6
+ * for markdown — browsers either show raw text or trigger a download
7
+ * (depending on the browser's text/markdown handling).
8
+ *
9
+ * This wrapper renders .md through MarkdownContent (same renderer as
10
+ * chat), and falls back to a plain <pre> for other text formats. It also
11
+ * exposes a one-click download link for any file type.
12
+ */
13
+
14
+ import ScratchViewer from '@/components/ScratchViewer';
15
+
16
+ export default async function ScratchPage({
17
+ params,
18
+ }: {
19
+ params: Promise<{ path: string[] }>;
20
+ }) {
21
+ const { path } = await params;
22
+ const joined = (path || []).map(encodeURIComponent).join('/');
23
+ return <ScratchViewer path={joined} />;
24
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import MarkdownContent from './MarkdownContent';
5
+
6
+ const TEXT_LIKE = new Set(['md', 'txt', 'log', 'json', 'yaml', 'yml', 'csv', 'html']);
7
+ const IMAGE_LIKE = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg']);
8
+ const EMBED_LIKE = new Set(['pdf']);
9
+
10
+ /** Cap inline text rendering. Anything larger falls back to a download
11
+ * prompt — react-markdown on multi-MB strings locks the tab. */
12
+ const MAX_INLINE_BYTES = 2 * 1024 * 1024;
13
+
14
+ function extOf(p: string): string {
15
+ const m = p.match(/\.([^./]+)$/);
16
+ return m ? m[1].toLowerCase() : '';
17
+ }
18
+
19
+ export default function ScratchViewer({ path }: { path: string }) {
20
+ const decoded = (() => {
21
+ try {
22
+ return decodeURIComponent(path);
23
+ } catch {
24
+ return path;
25
+ }
26
+ })();
27
+ const ext = extOf(decoded);
28
+ const rawUrl = `/api/scratch/${path}`;
29
+ const downloadUrl = `${rawUrl}?download=1`;
30
+
31
+ const [text, setText] = useState<string | null>(null);
32
+ const [err, setErr] = useState<string>('');
33
+ const [tooLarge, setTooLarge] = useState(false);
34
+
35
+ useEffect(() => {
36
+ if (!TEXT_LIKE.has(ext)) return;
37
+ let cancelled = false;
38
+ (async () => {
39
+ try {
40
+ const r = await fetch(rawUrl);
41
+ if (!r.ok) {
42
+ if (!cancelled) setErr(`${r.status} ${r.statusText}`);
43
+ return;
44
+ }
45
+ const len = Number(r.headers.get('content-length') || '0');
46
+ if (len > MAX_INLINE_BYTES) {
47
+ if (!cancelled) setTooLarge(true);
48
+ return;
49
+ }
50
+ const body = await r.text();
51
+ if (cancelled) return;
52
+ if (body.length > MAX_INLINE_BYTES) {
53
+ setTooLarge(true);
54
+ return;
55
+ }
56
+ setText(body);
57
+ } catch (e) {
58
+ if (!cancelled) setErr((e as Error).message);
59
+ }
60
+ })();
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, [rawUrl, ext]);
65
+
66
+ return (
67
+ <div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]">
68
+ <header className="sticky top-0 z-10 flex items-center justify-between gap-2 px-4 py-2 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
69
+ <div className="flex items-center gap-2 min-w-0">
70
+ <span className="text-[10px] uppercase tracking-wide text-[var(--text-secondary)]">scratch</span>
71
+ <span className="text-xs font-mono truncate" title={decoded}>
72
+ {decoded}
73
+ </span>
74
+ </div>
75
+ <div className="flex items-center gap-2 flex-shrink-0">
76
+ <a
77
+ href={rawUrl}
78
+ className="text-xs px-2 py-1 rounded border border-[var(--border)] hover:bg-[var(--bg-tertiary)]"
79
+ target="_blank"
80
+ rel="noopener"
81
+ >
82
+ Open raw
83
+ </a>
84
+ <a
85
+ href={downloadUrl}
86
+ className="text-xs px-2 py-1 rounded bg-[var(--accent)] text-white hover:opacity-90"
87
+ >
88
+ Download
89
+ </a>
90
+ </div>
91
+ </header>
92
+
93
+ <main className="px-4 py-3 max-w-4xl mx-auto">
94
+ {err && (
95
+ <div className="text-xs text-red-400 border border-red-400/40 rounded p-2 bg-red-400/5">
96
+ Failed to load: {err}
97
+ </div>
98
+ )}
99
+
100
+ {tooLarge && (
101
+ <div className="text-xs text-[var(--text-secondary)] border border-[var(--border)] rounded p-3">
102
+ File is larger than {Math.round(MAX_INLINE_BYTES / 1024 / 1024)} MB — inline preview skipped. Use Download or Open raw.
103
+ </div>
104
+ )}
105
+
106
+ {!err && !tooLarge && IMAGE_LIKE.has(ext) && (
107
+ <img src={rawUrl} alt={decoded} className="max-w-full rounded border border-[var(--border)]" />
108
+ )}
109
+
110
+ {!err && !tooLarge && EMBED_LIKE.has(ext) && (
111
+ <embed src={rawUrl} type="application/pdf" className="w-full h-[calc(100vh-60px)]" />
112
+ )}
113
+
114
+ {!err && !tooLarge && TEXT_LIKE.has(ext) && text != null && (
115
+ ext === 'md' ? (
116
+ <MarkdownContent content={text} />
117
+ ) : ext === 'html' ? (
118
+ // Wrap in iframe srcdoc so any embedded scripts can't reach Forge's
119
+ // origin or steal session cookies. sandbox blocks everything.
120
+ <iframe
121
+ srcDoc={text}
122
+ sandbox=""
123
+ className="w-full h-[calc(100vh-60px)] bg-white rounded border border-[var(--border)]"
124
+ title={decoded}
125
+ />
126
+ ) : (
127
+ <pre className="text-[12px] font-mono text-[var(--text-primary)] bg-[var(--bg-tertiary)] border border-[var(--border)] rounded p-3 overflow-auto whitespace-pre-wrap break-words" style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace' }}>
128
+ {text}
129
+ </pre>
130
+ )
131
+ )}
132
+
133
+ {!err && !tooLarge && !TEXT_LIKE.has(ext) && !IMAGE_LIKE.has(ext) && !EMBED_LIKE.has(ext) && (
134
+ <div className="text-xs text-[var(--text-secondary)] border border-[var(--border)] rounded p-3">
135
+ No inline preview for <code className="font-mono">.{ext || '?'}</code> files. Use Download.
136
+ </div>
137
+ )}
138
+ </main>
139
+ </div>
140
+ );
141
+ }
@@ -83,13 +83,13 @@ export const LINK_PATTERNS: LinkPattern[] = [
83
83
  },
84
84
  // Forge scratch-dir files. LLMs frequently emit paths like
85
85
  // `scratch/foo.md` when they write reports during chat-launched tasks.
86
- // Turn them into clickable links served by /api/scratch/<path>.
87
- // Match path segments + filename with extension; bound to a known set
88
- // of extensions to avoid linkifying noise like `scratch/notes`.
86
+ // Link to the in-browser viewer at /scratch/<path> (page renders .md
87
+ // through the chat markdown component + download button); the viewer
88
+ // itself fetches /api/scratch/<path> for raw bytes.
89
89
  {
90
90
  id: 'scratch-file',
91
91
  regex: /\bscratch\/([\w\-./]+?\.(?:md|txt|json|yaml|yml|csv|log|html|pdf|png|jpg|jpeg|gif|svg))\b/gi,
92
- url: '/api/scratch/{1}',
92
+ url: '/scratch/{1}',
93
93
  label: 'scratch/{1}',
94
94
  },
95
95
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.51",
3
+ "version": "0.10.53",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {