@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.
|
|
1
|
+
# Forge v0.10.53
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-09
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.52
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
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
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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: '/
|
|
92
|
+
url: '/scratch/{1}',
|
|
93
93
|
label: 'scratch/{1}',
|
|
94
94
|
},
|
|
95
95
|
];
|
package/package.json
CHANGED