@chrysb/alphaclaw 0.3.5-beta.0 → 0.3.5-beta.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.
- package/bin/alphaclaw.js +65 -1
- package/lib/public/css/explorer.css +201 -6
- package/lib/public/js/app.js +45 -1
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/file-tree.js +56 -67
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +164 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +59 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +77 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +83 -30
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +72 -11
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -0
- package/lib/public/js/lib/api.js +16 -0
- package/lib/public/js/lib/browse-file-policies.js +34 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +8 -0
- package/lib/server/routes/browse/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/{browse.js → browse/index.js} +290 -218
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -1095
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { LoadingSpinner } from "../loading-spinner.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const SqliteViewer = ({
|
|
8
|
+
sqliteSummary,
|
|
9
|
+
sqliteSelectedTable,
|
|
10
|
+
setSqliteSelectedTable,
|
|
11
|
+
sqliteTableOffset,
|
|
12
|
+
setSqliteTableOffset,
|
|
13
|
+
sqliteTableLoading,
|
|
14
|
+
sqliteTableError,
|
|
15
|
+
sqliteTableData,
|
|
16
|
+
kSqlitePageSize,
|
|
17
|
+
}) => {
|
|
18
|
+
const sqliteRows = Array.isArray(sqliteTableData?.rows) ? sqliteTableData.rows : [];
|
|
19
|
+
const sqliteColumns =
|
|
20
|
+
Array.isArray(sqliteTableData?.columns) && sqliteTableData.columns.length
|
|
21
|
+
? sqliteTableData.columns
|
|
22
|
+
: (sqliteSummary?.objects || []).find(
|
|
23
|
+
(entry) => entry?.name === sqliteSelectedTable,
|
|
24
|
+
)?.columns || [];
|
|
25
|
+
const sqliteTotalRows = Number(sqliteTableData?.totalRows || 0);
|
|
26
|
+
const sqliteCanGoPrev = sqliteTableOffset > 0;
|
|
27
|
+
const sqliteCanGoNext = sqliteTableOffset + kSqlitePageSize < sqliteTotalRows;
|
|
28
|
+
|
|
29
|
+
return html`
|
|
30
|
+
<div class="file-viewer-sqlite-shell">
|
|
31
|
+
${sqliteSummary?.objects?.length
|
|
32
|
+
? html`
|
|
33
|
+
<div class="file-viewer-sqlite-layout">
|
|
34
|
+
<div class="file-viewer-sqlite-list">
|
|
35
|
+
${sqliteSummary.objects.map(
|
|
36
|
+
(entry) => html`
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
class=${`file-viewer-sqlite-card ${sqliteSelectedTable === entry.name ? "is-active" : ""}`}
|
|
40
|
+
onclick=${() => {
|
|
41
|
+
if (!entry?.name || sqliteSelectedTable === entry.name) return;
|
|
42
|
+
setSqliteSelectedTable(entry.name);
|
|
43
|
+
setSqliteTableOffset(0);
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<div class="file-viewer-sqlite-title">
|
|
47
|
+
<span>${entry.name}</span>
|
|
48
|
+
<span class="file-viewer-sqlite-type">${entry.type}</span>
|
|
49
|
+
</div>
|
|
50
|
+
</button>
|
|
51
|
+
`,
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
<div class="file-viewer-sqlite-table-shell">
|
|
55
|
+
${sqliteSelectedTable
|
|
56
|
+
? html`
|
|
57
|
+
<div class="file-viewer-sqlite-table-header">
|
|
58
|
+
<span class="file-viewer-sqlite-table-name">
|
|
59
|
+
${sqliteSelectedTable}
|
|
60
|
+
</span>
|
|
61
|
+
<div class="file-viewer-sqlite-table-nav">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
class="ac-btn-secondary text-xs px-2 py-1 rounded-md"
|
|
65
|
+
disabled=${!sqliteCanGoPrev}
|
|
66
|
+
onclick=${() =>
|
|
67
|
+
setSqliteTableOffset((previousOffset) =>
|
|
68
|
+
Math.max(0, previousOffset - kSqlitePageSize),
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
Prev
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
class="ac-btn-secondary text-xs px-2 py-1 rounded-md"
|
|
76
|
+
disabled=${!sqliteCanGoNext}
|
|
77
|
+
onclick=${() =>
|
|
78
|
+
setSqliteTableOffset(
|
|
79
|
+
(previousOffset) => previousOffset + kSqlitePageSize,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
Next
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="file-viewer-sqlite-table-meta">
|
|
87
|
+
${sqliteTotalRows
|
|
88
|
+
? `${Math.min(sqliteTableOffset + 1, sqliteTotalRows)}-${Math.min(sqliteTableOffset + kSqlitePageSize, sqliteTotalRows)} of ${sqliteTotalRows} rows`
|
|
89
|
+
: "No rows"}
|
|
90
|
+
</div>
|
|
91
|
+
${sqliteTableLoading
|
|
92
|
+
? html`
|
|
93
|
+
<div class="file-viewer-loading-shell">
|
|
94
|
+
<${LoadingSpinner} className="h-4 w-4" />
|
|
95
|
+
</div>
|
|
96
|
+
`
|
|
97
|
+
: sqliteTableError
|
|
98
|
+
? html`
|
|
99
|
+
<div class="file-viewer-state file-viewer-state-error">
|
|
100
|
+
${sqliteTableError}
|
|
101
|
+
</div>
|
|
102
|
+
`
|
|
103
|
+
: html`
|
|
104
|
+
<div class="file-viewer-sqlite-table-wrap">
|
|
105
|
+
<table class="file-viewer-sqlite-table">
|
|
106
|
+
<thead>
|
|
107
|
+
<tr>
|
|
108
|
+
${sqliteColumns.map(
|
|
109
|
+
(column) => html`<th>${column.name}</th>`,
|
|
110
|
+
)}
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody>
|
|
114
|
+
${sqliteRows.length
|
|
115
|
+
? sqliteRows.map(
|
|
116
|
+
(row, rowIndex) => html`
|
|
117
|
+
<tr key=${rowIndex}>
|
|
118
|
+
${sqliteColumns.map((column) => {
|
|
119
|
+
const cellValue = row?.[column.name];
|
|
120
|
+
const displayValue =
|
|
121
|
+
cellValue === null
|
|
122
|
+
? "NULL"
|
|
123
|
+
: typeof cellValue === "object"
|
|
124
|
+
? JSON.stringify(cellValue)
|
|
125
|
+
: String(cellValue ?? "");
|
|
126
|
+
return html`
|
|
127
|
+
<td title=${displayValue}>
|
|
128
|
+
${displayValue}
|
|
129
|
+
</td>
|
|
130
|
+
`;
|
|
131
|
+
})}
|
|
132
|
+
</tr>
|
|
133
|
+
`,
|
|
134
|
+
)
|
|
135
|
+
: html`
|
|
136
|
+
<tr>
|
|
137
|
+
<td colspan=${Math.max(1, sqliteColumns.length)}>
|
|
138
|
+
<span class="file-viewer-sqlite-table-empty">
|
|
139
|
+
No rows
|
|
140
|
+
</span>
|
|
141
|
+
</td>
|
|
142
|
+
</tr>
|
|
143
|
+
`}
|
|
144
|
+
</tbody>
|
|
145
|
+
</table>
|
|
146
|
+
</div>
|
|
147
|
+
`}
|
|
148
|
+
`
|
|
149
|
+
: html`
|
|
150
|
+
<div class="file-viewer-state">Select a table to view rows.</div>
|
|
151
|
+
`}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="file-viewer-sqlite-footer">
|
|
155
|
+
${sqliteSummary.truncated
|
|
156
|
+
? `Showing ${sqliteSummary.objects.length} of ${sqliteSummary.totalObjects} tables/views`
|
|
157
|
+
: `${sqliteSummary.totalObjects} tables/views`}
|
|
158
|
+
</div>
|
|
159
|
+
`
|
|
160
|
+
: html`
|
|
161
|
+
<div class="file-viewer-state">
|
|
162
|
+
SQLite database loaded, but no tables/views were found.
|
|
163
|
+
</div>
|
|
164
|
+
`}
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "../action-button.js";
|
|
4
|
+
import { LockLineIcon } from "../icons.js";
|
|
5
|
+
|
|
6
|
+
const html = htm.bind(h);
|
|
7
|
+
|
|
8
|
+
export const FileViewerStatusBanners = ({
|
|
9
|
+
isDiffView,
|
|
10
|
+
onRequestEdit,
|
|
11
|
+
normalizedPath,
|
|
12
|
+
isLockedFile,
|
|
13
|
+
isProtectedFile,
|
|
14
|
+
isProtectedLocked,
|
|
15
|
+
handleEditProtectedFile,
|
|
16
|
+
}) => html`
|
|
17
|
+
${isDiffView
|
|
18
|
+
? html`
|
|
19
|
+
<div class="file-viewer-protected-banner file-viewer-diff-banner">
|
|
20
|
+
<div class="file-viewer-protected-banner-text">Viewing unsynced changes</div>
|
|
21
|
+
<${ActionButton}
|
|
22
|
+
onClick=${() => onRequestEdit(normalizedPath)}
|
|
23
|
+
tone="secondary"
|
|
24
|
+
size="sm"
|
|
25
|
+
idleLabel="View file"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
`
|
|
29
|
+
: null}
|
|
30
|
+
${!isDiffView && isLockedFile
|
|
31
|
+
? html`
|
|
32
|
+
<div class="file-viewer-protected-banner is-locked">
|
|
33
|
+
<${LockLineIcon} className="file-viewer-protected-banner-icon" />
|
|
34
|
+
<div class="file-viewer-protected-banner-text">
|
|
35
|
+
This file is managed by AlphaClaw and cannot be edited.
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
`
|
|
39
|
+
: null}
|
|
40
|
+
${!isDiffView && isProtectedFile
|
|
41
|
+
? html`
|
|
42
|
+
<div class="file-viewer-protected-banner">
|
|
43
|
+
<div class="file-viewer-protected-banner-text">
|
|
44
|
+
Protected file. Changes may break workspace behavior.
|
|
45
|
+
</div>
|
|
46
|
+
${isProtectedLocked
|
|
47
|
+
? html`
|
|
48
|
+
<${ActionButton}
|
|
49
|
+
onClick=${handleEditProtectedFile}
|
|
50
|
+
tone="warning"
|
|
51
|
+
size="sm"
|
|
52
|
+
idleLabel="Edit anyway"
|
|
53
|
+
/>
|
|
54
|
+
`
|
|
55
|
+
: null}
|
|
56
|
+
</div>
|
|
57
|
+
`
|
|
58
|
+
: null}
|
|
59
|
+
`;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
kEditorSelectionStorageKey,
|
|
3
|
+
kFileViewerModeStorageKey,
|
|
4
|
+
kLegacyFileViewerModeStorageKey,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
|
|
7
|
+
export const readStoredFileViewerMode = () => {
|
|
8
|
+
try {
|
|
9
|
+
const storedMode = String(
|
|
10
|
+
window.localStorage.getItem(kFileViewerModeStorageKey) ||
|
|
11
|
+
window.localStorage.getItem(kLegacyFileViewerModeStorageKey) ||
|
|
12
|
+
"",
|
|
13
|
+
).trim();
|
|
14
|
+
return storedMode === "preview" ? "preview" : "edit";
|
|
15
|
+
} catch {
|
|
16
|
+
return "edit";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const readEditorSelectionStorageMap = () => {
|
|
21
|
+
try {
|
|
22
|
+
const rawStorageValue = window.localStorage.getItem(kEditorSelectionStorageKey);
|
|
23
|
+
if (!rawStorageValue) return {};
|
|
24
|
+
const parsedStorageValue = JSON.parse(rawStorageValue);
|
|
25
|
+
if (!parsedStorageValue || typeof parsedStorageValue !== "object") return {};
|
|
26
|
+
return parsedStorageValue;
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const readStoredEditorSelection = (filePath) => {
|
|
33
|
+
const safePath = String(filePath || "").trim();
|
|
34
|
+
if (!safePath) return null;
|
|
35
|
+
const storageMap = readEditorSelectionStorageMap();
|
|
36
|
+
const selection = storageMap[safePath];
|
|
37
|
+
if (!selection || typeof selection !== "object") return null;
|
|
38
|
+
return {
|
|
39
|
+
start: selection.start,
|
|
40
|
+
end: selection.end,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const writeStoredEditorSelection = (filePath, selection) => {
|
|
45
|
+
const safePath = String(filePath || "").trim();
|
|
46
|
+
if (!safePath || !selection || typeof selection !== "object") return;
|
|
47
|
+
try {
|
|
48
|
+
const nextStorageValue = readEditorSelectionStorageMap();
|
|
49
|
+
nextStorageValue[safePath] = {
|
|
50
|
+
start: selection.start,
|
|
51
|
+
end: selection.end,
|
|
52
|
+
};
|
|
53
|
+
window.localStorage.setItem(
|
|
54
|
+
kEditorSelectionStorageKey,
|
|
55
|
+
JSON.stringify(nextStorageValue),
|
|
56
|
+
);
|
|
57
|
+
} catch {}
|
|
58
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "../action-button.js";
|
|
4
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
+
import { SaveFillIcon } from "../icons.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
export const FileViewerToolbar = ({
|
|
10
|
+
pathSegments,
|
|
11
|
+
isDirty,
|
|
12
|
+
isPreviewOnly,
|
|
13
|
+
isDiffView,
|
|
14
|
+
isMarkdownFile,
|
|
15
|
+
viewMode,
|
|
16
|
+
handleChangeViewMode,
|
|
17
|
+
handleSave,
|
|
18
|
+
loading,
|
|
19
|
+
canEditFile,
|
|
20
|
+
isEditBlocked,
|
|
21
|
+
isImageFile,
|
|
22
|
+
isAudioFile,
|
|
23
|
+
isSqliteFile,
|
|
24
|
+
saving,
|
|
25
|
+
}) => html`
|
|
26
|
+
<div class="file-viewer-tabbar">
|
|
27
|
+
<div class="file-viewer-tab active">
|
|
28
|
+
<span class="file-icon">f</span>
|
|
29
|
+
<span class="file-viewer-breadcrumb">
|
|
30
|
+
${pathSegments.map(
|
|
31
|
+
(segment, index) => html`
|
|
32
|
+
<span class="file-viewer-breadcrumb-item">
|
|
33
|
+
<span class=${index === pathSegments.length - 1 ? "is-current" : ""}>
|
|
34
|
+
${segment}
|
|
35
|
+
</span>
|
|
36
|
+
${index < pathSegments.length - 1 && html`<span class="file-viewer-sep">></span>`}
|
|
37
|
+
</span>
|
|
38
|
+
`,
|
|
39
|
+
)}
|
|
40
|
+
</span>
|
|
41
|
+
${isDirty ? html`<span class="file-viewer-dirty-dot" aria-hidden="true"></span>` : null}
|
|
42
|
+
</div>
|
|
43
|
+
<div class="file-viewer-tabbar-spacer"></div>
|
|
44
|
+
${isPreviewOnly ? html`<div class="file-viewer-preview-pill">Preview</div>` : null}
|
|
45
|
+
${!isDiffView &&
|
|
46
|
+
isMarkdownFile &&
|
|
47
|
+
html`
|
|
48
|
+
<${SegmentedControl}
|
|
49
|
+
className="mr-2.5"
|
|
50
|
+
options=${[
|
|
51
|
+
{ label: "edit", value: "edit" },
|
|
52
|
+
{ label: "preview", value: "preview" },
|
|
53
|
+
]}
|
|
54
|
+
value=${viewMode}
|
|
55
|
+
onChange=${handleChangeViewMode}
|
|
56
|
+
/>
|
|
57
|
+
`}
|
|
58
|
+
${!isDiffView
|
|
59
|
+
? !isImageFile && !isAudioFile && !isSqliteFile
|
|
60
|
+
? html`
|
|
61
|
+
<${ActionButton}
|
|
62
|
+
onClick=${handleSave}
|
|
63
|
+
disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
|
|
64
|
+
loading=${saving}
|
|
65
|
+
tone=${isDirty ? "primary" : "secondary"}
|
|
66
|
+
size="sm"
|
|
67
|
+
idleLabel="Save"
|
|
68
|
+
loadingLabel="Saving..."
|
|
69
|
+
idleIcon=${SaveFillIcon}
|
|
70
|
+
idleIconClassName="file-viewer-save-icon"
|
|
71
|
+
className="file-viewer-save-action"
|
|
72
|
+
/>
|
|
73
|
+
`
|
|
74
|
+
: null
|
|
75
|
+
: null}
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useEffect } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { readStoredEditorSelection } from "./storage.js";
|
|
3
|
+
import { clampSelectionIndex } from "./utils.js";
|
|
4
|
+
import { getScrollRatio } from "./scroll-sync.js";
|
|
5
|
+
|
|
6
|
+
export const useEditorSelectionRestore = ({
|
|
7
|
+
canEditFile,
|
|
8
|
+
loading,
|
|
9
|
+
hasSelectedPath,
|
|
10
|
+
normalizedPath,
|
|
11
|
+
loadedFilePathRef,
|
|
12
|
+
restoredSelectionPathRef,
|
|
13
|
+
viewMode,
|
|
14
|
+
content,
|
|
15
|
+
editorTextareaRef,
|
|
16
|
+
editorLineNumbersRef,
|
|
17
|
+
editorHighlightRef,
|
|
18
|
+
viewScrollRatioRef,
|
|
19
|
+
}) => {
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!canEditFile || loading || !hasSelectedPath) return () => {};
|
|
22
|
+
if (loadedFilePathRef.current !== normalizedPath) return () => {};
|
|
23
|
+
if (restoredSelectionPathRef.current === normalizedPath) return () => {};
|
|
24
|
+
if (viewMode !== "edit") return () => {};
|
|
25
|
+
const storedSelection = readStoredEditorSelection(normalizedPath);
|
|
26
|
+
if (!storedSelection) {
|
|
27
|
+
restoredSelectionPathRef.current = normalizedPath;
|
|
28
|
+
return () => {};
|
|
29
|
+
}
|
|
30
|
+
let frameId = 0;
|
|
31
|
+
let attempts = 0;
|
|
32
|
+
const restoreSelection = () => {
|
|
33
|
+
const textareaElement = editorTextareaRef.current;
|
|
34
|
+
if (!textareaElement) {
|
|
35
|
+
attempts += 1;
|
|
36
|
+
if (attempts < 6) frameId = window.requestAnimationFrame(restoreSelection);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const maxIndex = String(content || "").length;
|
|
40
|
+
const start = clampSelectionIndex(storedSelection.start, maxIndex);
|
|
41
|
+
const end = clampSelectionIndex(storedSelection.end, maxIndex);
|
|
42
|
+
textareaElement.focus();
|
|
43
|
+
textareaElement.setSelectionRange(start, Math.max(start, end));
|
|
44
|
+
window.requestAnimationFrame(() => {
|
|
45
|
+
const nextTextareaElement = editorTextareaRef.current;
|
|
46
|
+
if (!nextTextareaElement) return;
|
|
47
|
+
const safeContent = String(content || "");
|
|
48
|
+
const safeStart = clampSelectionIndex(start, safeContent.length);
|
|
49
|
+
const lineIndex = safeContent.slice(0, safeStart).split("\n").length - 1;
|
|
50
|
+
const computedStyle = window.getComputedStyle(nextTextareaElement);
|
|
51
|
+
const parsedLineHeight = Number.parseFloat(computedStyle.lineHeight || "");
|
|
52
|
+
const lineHeight =
|
|
53
|
+
Number.isFinite(parsedLineHeight) && parsedLineHeight > 0 ? parsedLineHeight : 20;
|
|
54
|
+
const nextScrollTop = Math.max(
|
|
55
|
+
0,
|
|
56
|
+
lineIndex * lineHeight - nextTextareaElement.clientHeight * 0.4,
|
|
57
|
+
);
|
|
58
|
+
nextTextareaElement.scrollTop = nextScrollTop;
|
|
59
|
+
if (editorLineNumbersRef.current) {
|
|
60
|
+
editorLineNumbersRef.current.scrollTop = nextScrollTop;
|
|
61
|
+
}
|
|
62
|
+
if (editorHighlightRef.current) {
|
|
63
|
+
editorHighlightRef.current.scrollTop = nextScrollTop;
|
|
64
|
+
}
|
|
65
|
+
viewScrollRatioRef.current = getScrollRatio(nextTextareaElement);
|
|
66
|
+
});
|
|
67
|
+
restoredSelectionPathRef.current = normalizedPath;
|
|
68
|
+
};
|
|
69
|
+
frameId = window.requestAnimationFrame(restoreSelection);
|
|
70
|
+
return () => {
|
|
71
|
+
if (frameId) window.cancelAnimationFrame(frameId);
|
|
72
|
+
};
|
|
73
|
+
}, [
|
|
74
|
+
canEditFile,
|
|
75
|
+
loading,
|
|
76
|
+
hasSelectedPath,
|
|
77
|
+
normalizedPath,
|
|
78
|
+
content,
|
|
79
|
+
viewMode,
|
|
80
|
+
loadedFilePathRef,
|
|
81
|
+
restoredSelectionPathRef,
|
|
82
|
+
editorTextareaRef,
|
|
83
|
+
editorLineNumbersRef,
|
|
84
|
+
editorHighlightRef,
|
|
85
|
+
viewScrollRatioRef,
|
|
86
|
+
]);
|
|
87
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { fetchBrowseFileDiff } from "../../lib/api.js";
|
|
3
|
+
|
|
4
|
+
export const useFileDiff = ({
|
|
5
|
+
hasSelectedPath,
|
|
6
|
+
isDiffView,
|
|
7
|
+
isPreviewOnly,
|
|
8
|
+
normalizedPath,
|
|
9
|
+
}) => {
|
|
10
|
+
const [diffLoading, setDiffLoading] = useState(false);
|
|
11
|
+
const [diffError, setDiffError] = useState("");
|
|
12
|
+
const [diffContent, setDiffContent] = useState("");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
let active = true;
|
|
16
|
+
if (!hasSelectedPath || !isDiffView || isPreviewOnly) {
|
|
17
|
+
setDiffLoading(false);
|
|
18
|
+
setDiffError("");
|
|
19
|
+
setDiffContent("");
|
|
20
|
+
return () => {
|
|
21
|
+
active = false;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const loadDiff = async () => {
|
|
25
|
+
setDiffLoading(true);
|
|
26
|
+
setDiffError("");
|
|
27
|
+
try {
|
|
28
|
+
const data = await fetchBrowseFileDiff(normalizedPath);
|
|
29
|
+
if (!active) return;
|
|
30
|
+
setDiffContent(String(data?.content || ""));
|
|
31
|
+
} catch (nextError) {
|
|
32
|
+
if (!active) return;
|
|
33
|
+
setDiffError(nextError.message || "Could not load diff");
|
|
34
|
+
} finally {
|
|
35
|
+
if (active) setDiffLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
loadDiff();
|
|
39
|
+
return () => {
|
|
40
|
+
active = false;
|
|
41
|
+
};
|
|
42
|
+
}, [hasSelectedPath, isDiffView, isPreviewOnly, normalizedPath]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
diffLoading,
|
|
46
|
+
diffError,
|
|
47
|
+
diffContent,
|
|
48
|
+
};
|
|
49
|
+
};
|