dsh-prompt-vcs 0.2.1 → 0.3.0
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/lib/client/locales.d.ts +14 -0
- package/lib/client/locales.js +28 -0
- package/lib/client/view.d.ts +0 -3
- package/lib/client/view.js +66 -27
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/routes.d.ts +3 -0
- package/lib/routes.js +19 -0
- package/lib/types.d.ts +9 -0
- package/lib/vcs.d.ts +5 -1
- package/lib/vcs.js +32 -1
- package/lib/vcs.web.js +190 -40
- package/lib/vcs.web.js.map +2 -2
- package/package.json +1 -1
package/lib/client/locales.d.ts
CHANGED
|
@@ -24,6 +24,20 @@ export declare const zh: {
|
|
|
24
24
|
loading: string;
|
|
25
25
|
failed: string;
|
|
26
26
|
retry: string;
|
|
27
|
+
totalChanges: string;
|
|
28
|
+
filesTracked: string;
|
|
29
|
+
watched: string;
|
|
30
|
+
files: string;
|
|
31
|
+
changes: string;
|
|
32
|
+
lastChanged: string;
|
|
33
|
+
size: string;
|
|
34
|
+
timeline: string;
|
|
35
|
+
diff: string;
|
|
36
|
+
before: string;
|
|
37
|
+
after: string;
|
|
38
|
+
rollback: string;
|
|
39
|
+
confirmRollback: string;
|
|
40
|
+
notFound: string;
|
|
27
41
|
};
|
|
28
42
|
/** English dictionary, checked complete against the zh key set. */
|
|
29
43
|
export declare const en: typeof zh;
|
package/lib/client/locales.js
CHANGED
|
@@ -24,6 +24,20 @@ export const zh = {
|
|
|
24
24
|
'loading': '正在加载…',
|
|
25
25
|
'failed': '加载失败',
|
|
26
26
|
'retry': '重试',
|
|
27
|
+
'totalChanges': '总变更数',
|
|
28
|
+
'filesTracked': '跟踪文件数',
|
|
29
|
+
'watched': '监控文件',
|
|
30
|
+
'files': '文件',
|
|
31
|
+
'changes': '变更次数',
|
|
32
|
+
'lastChanged': '最后变更',
|
|
33
|
+
'size': '大小',
|
|
34
|
+
'timeline': '时间线',
|
|
35
|
+
'diff': '差异',
|
|
36
|
+
'before': '变更前',
|
|
37
|
+
'after': '变更后',
|
|
38
|
+
'rollback': '回滚',
|
|
39
|
+
'confirmRollback': '确定回滚到此变更之前的状态?',
|
|
40
|
+
'notFound': '未找到变更记录',
|
|
27
41
|
};
|
|
28
42
|
/** English dictionary, checked complete against the zh key set. */
|
|
29
43
|
export const en = {
|
|
@@ -40,4 +54,18 @@ export const en = {
|
|
|
40
54
|
'loading': 'Loading…',
|
|
41
55
|
'failed': 'Failed to load',
|
|
42
56
|
'retry': 'Retry',
|
|
57
|
+
'totalChanges': 'Total Changes',
|
|
58
|
+
'filesTracked': 'Files Tracked',
|
|
59
|
+
'watched': 'Watched',
|
|
60
|
+
'files': 'Files',
|
|
61
|
+
'changes': 'Changes',
|
|
62
|
+
'lastChanged': 'Last Changed',
|
|
63
|
+
'size': 'Size',
|
|
64
|
+
'timeline': 'Timeline',
|
|
65
|
+
'diff': 'Diff',
|
|
66
|
+
'before': 'Before',
|
|
67
|
+
'after': 'After',
|
|
68
|
+
'rollback': 'Rollback',
|
|
69
|
+
'confirmRollback': 'Rollback to before this change?',
|
|
70
|
+
'notFound': 'Change not found',
|
|
43
71
|
};
|
package/lib/client/view.d.ts
CHANGED
|
@@ -9,17 +9,14 @@
|
|
|
9
9
|
* @module client/view
|
|
10
10
|
*/
|
|
11
11
|
import type { PanelPayload } from '../types.js';
|
|
12
|
-
/** The translate seat the renderer binds from this plugin's locale namespace. */
|
|
13
12
|
export type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
14
13
|
export interface PanelProps {
|
|
15
|
-
/** Bound translate function for this plugin's namespace. */
|
|
16
14
|
t: Translate;
|
|
17
15
|
}
|
|
18
16
|
interface PanelState {
|
|
19
17
|
payload: PanelPayload | null;
|
|
20
18
|
error: string | null;
|
|
21
19
|
}
|
|
22
|
-
/** Fetch the host panel payload; `reload` re-runs the request. */
|
|
23
20
|
export declare function usePanel(): PanelState & {
|
|
24
21
|
reload: () => void;
|
|
25
22
|
};
|
package/lib/client/view.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Pure rendering half of the promptVcs page.
|
|
4
4
|
*
|
|
@@ -10,48 +10,87 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* @module client/view
|
|
11
11
|
*/
|
|
12
12
|
import { useCallback, useEffect, useState } from 'react';
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const PANEL_PATH = '/api/vcs.panel';
|
|
14
|
+
const CHANGE_PATH = '/api/vcs.change';
|
|
15
|
+
const ROLLBACK_PATH = '/api/vcs.rollback';
|
|
16
|
+
const wrap = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' };
|
|
16
17
|
const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
|
|
17
18
|
const muted = { fontSize: 12, opacity: 0.75 };
|
|
18
19
|
const table = { borderCollapse: 'collapse', width: '100%' };
|
|
19
20
|
const th = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' };
|
|
20
21
|
const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
|
|
21
|
-
|
|
22
|
+
const card = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 };
|
|
23
|
+
const btn = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' };
|
|
24
|
+
const dangerBtn = { ...btn, color: '#e55', borderColor: '#e55' };
|
|
25
|
+
const preBlock = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 11, maxHeight: 200, overflow: 'auto', padding: 8, borderRadius: 4, background: 'rgba(128,128,128,0.06)', border: '1px solid rgba(128,128,128,0.15)' };
|
|
26
|
+
const clickRow = { cursor: 'pointer' };
|
|
27
|
+
const statRow = { display: 'flex', gap: 12, flexWrap: 'wrap' };
|
|
28
|
+
const statBox = { ...card, flex: 1, minWidth: 100, textAlign: 'center' };
|
|
22
29
|
export function usePanel() {
|
|
23
30
|
const [state, setState] = useState({ payload: null, error: null });
|
|
24
31
|
const [tick, setTick] = useState(0);
|
|
25
|
-
const reload = useCallback(() => setTick((
|
|
32
|
+
const reload = useCallback(() => setTick((v) => v + 1), []);
|
|
26
33
|
useEffect(() => {
|
|
27
|
-
const
|
|
28
|
-
setState((
|
|
29
|
-
fetch(PANEL_PATH, { signal:
|
|
30
|
-
.then(async (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
setState({ payload, error: null });
|
|
38
|
-
})
|
|
39
|
-
.catch((cause) => {
|
|
40
|
-
if (controller.signal.aborted)
|
|
41
|
-
return;
|
|
42
|
-
setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
|
|
43
|
-
});
|
|
44
|
-
return () => controller.abort();
|
|
34
|
+
const c = new AbortController();
|
|
35
|
+
setState((p) => ({ ...p, error: null }));
|
|
36
|
+
fetch(PANEL_PATH, { signal: c.signal })
|
|
37
|
+
.then(async (r) => { if (!r.ok)
|
|
38
|
+
throw new Error(String(r.status)); return r.json(); })
|
|
39
|
+
.then((payload) => { if (!c.signal.aborted)
|
|
40
|
+
setState({ payload, error: null }); })
|
|
41
|
+
.catch((e) => { if (!c.signal.aborted)
|
|
42
|
+
setState({ payload: null, error: e instanceof Error ? e.message : String(e) }); });
|
|
43
|
+
return () => c.abort();
|
|
45
44
|
}, [tick]);
|
|
46
45
|
return { ...state, reload };
|
|
47
46
|
}
|
|
47
|
+
// --- Change detail expansion with diff and rollback ---
|
|
48
|
+
function ChangeDetail({ hash, t, onRollback }) {
|
|
49
|
+
const [change, setChange] = useState(null);
|
|
50
|
+
const [loading, setLoading] = useState(false);
|
|
51
|
+
const [rolling, setRolling] = useState(false);
|
|
52
|
+
const load = useCallback(async () => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch(`${CHANGE_PATH}?hash=${hash}`);
|
|
56
|
+
if (r.ok)
|
|
57
|
+
setChange(await r.json());
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, [hash]);
|
|
63
|
+
useEffect(() => { load(); }, [load]);
|
|
64
|
+
const handleRollback = useCallback(async () => {
|
|
65
|
+
if (!confirm(t('confirmRollback')))
|
|
66
|
+
return;
|
|
67
|
+
setRolling(true);
|
|
68
|
+
try {
|
|
69
|
+
await fetch(ROLLBACK_PATH, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ hash }),
|
|
73
|
+
});
|
|
74
|
+
onRollback();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
setRolling(false);
|
|
78
|
+
}
|
|
79
|
+
}, [hash, t, onRollback]);
|
|
80
|
+
if (loading)
|
|
81
|
+
return _jsx("p", { style: muted, children: t('loading') });
|
|
82
|
+
if (!change)
|
|
83
|
+
return _jsx("p", { style: muted, children: t('notFound') });
|
|
84
|
+
return (_jsxs("div", { style: { ...card, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("strong", { children: change.file }), _jsx("button", { type: "button", style: dangerBtn, disabled: rolling, onClick: handleRollback, children: rolling ? '…' : `↩ ${t('rollback')}` })] }), _jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("strong", { style: { fontSize: 12 }, children: t('diff') }), _jsx("pre", { style: preBlock, children: change.diff })] }), _jsxs("div", { style: { display: 'flex', gap: 8, flexWrap: 'wrap' }, children: [_jsxs("div", { style: { flex: 1, minWidth: 250 }, children: [_jsx("strong", { style: { fontSize: 12, color: '#e55' }, children: t('before') }), _jsxs("pre", { style: { ...preBlock, maxHeight: 150 }, children: [change.oldContent.slice(0, 2000), change.oldContent.length > 2000 ? '\n…' : ''] })] }), _jsxs("div", { style: { flex: 1, minWidth: 250 }, children: [_jsx("strong", { style: { fontSize: 12, color: '#4a4' }, children: t('after') }), _jsxs("pre", { style: { ...preBlock, maxHeight: 150 }, children: [change.newContent.slice(0, 2000), change.newContent.length > 2000 ? '\n…' : ''] })] })] })] }));
|
|
85
|
+
}
|
|
48
86
|
export function PromptVcsPanel({ t }) {
|
|
49
87
|
const { payload, error, reload } = usePanel();
|
|
50
|
-
const
|
|
88
|
+
const [expandedHash, setExpandedHash] = useState(null);
|
|
89
|
+
const header = (_jsxs("header", { style: head, children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDCDD ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", style: btn, onClick: reload, children: t('refresh') })] }));
|
|
51
90
|
if (error !== null) {
|
|
52
|
-
return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { alignSelf: 'flex-start'
|
|
91
|
+
return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: 'flex-start' }, children: t('retry') })] }));
|
|
53
92
|
}
|
|
54
93
|
if (payload === null)
|
|
55
94
|
return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
|
|
56
|
-
return (_jsxs("div", { style: wrap, children: [header, payload.timeline.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: t('hash') }), _jsx("th", { style: th, children: t('date') }), _jsx("th", { style: th, children: t('by') }), _jsx("th", { style: th, children: t('file') }), _jsx("th", { style: th, children: t('added') }), _jsx("th", { style: th, children: t('removed') })] }) }), _jsx("tbody", { children: payload.timeline.map((row) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: row.hash }) }), _jsx("td", { style: td, children: new Date(row.timestamp).toLocaleString() }), _jsx("td", { style: td, children: row.changedBy }), _jsx("td", { style: td, children: row.file }), _jsxs("td", { style: td, children: ["+", row.addedLines] }), _jsxs("td", { style: td, children: ["-", row.removedLines] })] }, row.hash))) })] }))] }));
|
|
95
|
+
return (_jsxs("div", { style: wrap, children: [header, _jsxs("div", { style: statRow, children: [_jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: payload.totalChanges }), _jsx("div", { style: muted, children: t('totalChanges') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 18, fontWeight: 700 }, children: payload.files.length }), _jsx("div", { style: muted, children: t('filesTracked') })] }), _jsxs("div", { style: statBox, children: [_jsx("div", { style: { fontSize: 14, fontWeight: 600 }, children: payload.watchedFiles.join(', ') }), _jsx("div", { style: muted, children: t('watched') })] })] }), payload.files.length > 0 && (_jsxs("div", { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDCC1 ", t('files')] }), _jsxs("table", { style: { ...table, marginTop: 4 }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: t('file') }), _jsx("th", { style: th, children: t('changes') }), _jsx("th", { style: th, children: t('lastChanged') }), _jsx("th", { style: th, children: t('size') })] }) }), _jsx("tbody", { children: payload.files.map((f) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: f.file }) }), _jsx("td", { style: td, children: f.changes }), _jsx("td", { style: td, children: new Date(f.lastChanged).toLocaleString() }), _jsxs("td", { style: td, children: [f.currentSize, "B"] })] }, f.file))) })] })] })), payload.timeline.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("div", { children: [_jsxs("strong", { style: { fontSize: 13 }, children: ["\uD83D\uDD52 ", t('timeline')] }), _jsxs("table", { style: { ...table, marginTop: 4 }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th }), _jsx("th", { style: th, children: t('hash') }), _jsx("th", { style: th, children: t('date') }), _jsx("th", { style: th, children: t('by') }), _jsx("th", { style: th, children: t('file') }), _jsx("th", { style: th, children: t('added') }), _jsx("th", { style: th, children: t('removed') })] }) }), _jsx("tbody", { children: payload.timeline.map((row) => (_jsxs(_Fragment, { children: [_jsxs("tr", { style: clickRow, onClick: () => setExpandedHash(expandedHash === row.hash ? null : row.hash), children: [_jsx("td", { style: td, children: expandedHash === row.hash ? '▼' : '▶' }), _jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: row.hash }) }), _jsx("td", { style: td, children: new Date(row.timestamp).toLocaleString() }), _jsx("td", { style: td, children: row.changedBy }), _jsx("td", { style: td, children: row.file }), _jsxs("td", { style: { ...td, color: '#4a4' }, children: ["+", row.addedLines] }), _jsxs("td", { style: { ...td, color: '#e55' }, children: ["-", row.removedLines] })] }, row.hash), expandedHash === row.hash && (_jsx("tr", { children: _jsx("td", { colSpan: 7, style: { padding: '4px 0' }, children: _jsx(ChangeDetail, { hash: row.hash, t: t, onRollback: () => { setExpandedHash(null); reload(); } }) }) }, `${row.hash}-detail`))] }))) })] })] }))] }));
|
|
57
96
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export interface VcsService {
|
|
|
5
5
|
recordChange(file: string, oldContent: string, newContent: string, changedBy?: 'user' | 'plugin' | 'agent', pluginId?: string): ReturnType<PromptVcs['recordChange']>;
|
|
6
6
|
timeline(): ReturnType<PromptVcs['timeline']>;
|
|
7
7
|
getDiff(hash: string): ReturnType<PromptVcs['getDiff']>;
|
|
8
|
+
getChange(hash: string): ReturnType<PromptVcs['getChange']>;
|
|
8
9
|
rollback(hash: string): boolean;
|
|
9
10
|
panel(): ReturnType<PromptVcs['panel']>;
|
|
11
|
+
fileStats(): ReturnType<PromptVcs['fileStats']>;
|
|
10
12
|
}
|
|
11
13
|
export declare function apply(ctx: Context): void;
|
package/lib/index.js
CHANGED
|
@@ -9,8 +9,10 @@ export function apply(ctx) {
|
|
|
9
9
|
recordChange: (file, oldContent, newContent, changedBy, pluginId) => vcs.recordChange(file, oldContent, newContent, changedBy, pluginId),
|
|
10
10
|
timeline: () => vcs.timeline(),
|
|
11
11
|
getDiff: (hash) => vcs.getDiff(hash),
|
|
12
|
+
getChange: (hash) => vcs.getChange(hash),
|
|
12
13
|
rollback: (hash) => vcs.rollback(hash),
|
|
13
14
|
panel: () => vcs.panel(),
|
|
15
|
+
fileStats: () => vcs.fileStats(),
|
|
14
16
|
};
|
|
15
17
|
ctx.provide('promptVcs', service);
|
|
16
18
|
registerVcsRoutes(ctx, service);
|
package/lib/routes.d.ts
CHANGED
|
@@ -2,4 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import type { VcsService } from './index.js';
|
|
3
3
|
import { VCS_PANEL_PATH } from './vcs.js';
|
|
4
4
|
export { VCS_PANEL_PATH };
|
|
5
|
+
export declare const VCS_DIFF_PATH = "/api/vcs.diff";
|
|
6
|
+
export declare const VCS_ROLLBACK_PATH = "/api/vcs.rollback";
|
|
7
|
+
export declare const VCS_CHANGE_PATH = "/api/vcs.change";
|
|
5
8
|
export declare function registerVcsRoutes(ctx: Context, vcs: VcsService): void;
|
package/lib/routes.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { VCS_PANEL_PATH } from './vcs.js';
|
|
2
2
|
export { VCS_PANEL_PATH };
|
|
3
|
+
export const VCS_DIFF_PATH = '/api/vcs.diff';
|
|
4
|
+
export const VCS_ROLLBACK_PATH = '/api/vcs.rollback';
|
|
5
|
+
export const VCS_CHANGE_PATH = '/api/vcs.change';
|
|
3
6
|
export function registerVcsRoutes(ctx, vcs) {
|
|
4
7
|
ctx.inject(['connection'], (connectionCtx) => {
|
|
5
8
|
const connection = connectionCtx.connection;
|
|
@@ -46,5 +49,21 @@ export function registerVcsRoutes(ctx, vcs) {
|
|
|
46
49
|
return Response.json({ ok: true });
|
|
47
50
|
},
|
|
48
51
|
});
|
|
52
|
+
// Full change detail (with old/new content)
|
|
53
|
+
connection.fetch.register({
|
|
54
|
+
path: VCS_CHANGE_PATH,
|
|
55
|
+
methods: ['GET'],
|
|
56
|
+
requestBody: 'buffered',
|
|
57
|
+
fetch: async (request) => {
|
|
58
|
+
const url = new URL(request.url);
|
|
59
|
+
const hash = url.searchParams.get('hash');
|
|
60
|
+
if (!hash)
|
|
61
|
+
return Response.json({ error: 'missing hash' }, { status: 400 });
|
|
62
|
+
const change = vcs.getChange(hash);
|
|
63
|
+
if (!change)
|
|
64
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
65
|
+
return Response.json(change, { headers: { 'cache-control': 'no-store' } });
|
|
66
|
+
},
|
|
67
|
+
});
|
|
49
68
|
});
|
|
50
69
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -18,6 +18,15 @@ export interface TimelineEntry {
|
|
|
18
18
|
addedLines: number;
|
|
19
19
|
removedLines: number;
|
|
20
20
|
}
|
|
21
|
+
export interface FileStat {
|
|
22
|
+
file: string;
|
|
23
|
+
changes: number;
|
|
24
|
+
lastChanged: number;
|
|
25
|
+
currentSize: number;
|
|
26
|
+
}
|
|
21
27
|
export interface PanelPayload {
|
|
22
28
|
timeline: TimelineEntry[];
|
|
29
|
+
files: FileStat[];
|
|
30
|
+
totalChanges: number;
|
|
31
|
+
watchedFiles: string[];
|
|
23
32
|
}
|
package/lib/vcs.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Change, TimelineEntry, PanelPayload } from './types.js';
|
|
1
|
+
import type { Change, TimelineEntry, PanelPayload, FileStat } from './types.js';
|
|
2
2
|
export declare const VCS_PANEL_PATH = "/api/vcs.panel";
|
|
3
3
|
export declare class PromptVcs {
|
|
4
4
|
private projectDir;
|
|
@@ -10,8 +10,12 @@ export declare class PromptVcs {
|
|
|
10
10
|
checkFile(file: string, lastKnownContent: string | null): Change | null;
|
|
11
11
|
timeline(): TimelineEntry[];
|
|
12
12
|
getDiff(hash: string): string | null;
|
|
13
|
+
/** Get full change detail by hash. */
|
|
14
|
+
getChange(hash: string): Change | null;
|
|
13
15
|
/** Rollback a file to its state before the given change. */
|
|
14
16
|
rollback(hash: string): boolean;
|
|
17
|
+
/** Get stats per file. */
|
|
18
|
+
fileStats(): FileStat[];
|
|
15
19
|
panel(): PanelPayload;
|
|
16
20
|
static get watchedFiles(): string[];
|
|
17
21
|
}
|
package/lib/vcs.js
CHANGED
|
@@ -59,6 +59,11 @@ export class PromptVcs {
|
|
|
59
59
|
const change = this.store.readAll().find((c) => c.hash === hash);
|
|
60
60
|
return change?.diff ?? null;
|
|
61
61
|
}
|
|
62
|
+
/** Get full change detail by hash. */
|
|
63
|
+
getChange(hash) {
|
|
64
|
+
const change = this.store.readAll().find((c) => c.hash === hash);
|
|
65
|
+
return change ?? null;
|
|
66
|
+
}
|
|
62
67
|
/** Rollback a file to its state before the given change. */
|
|
63
68
|
rollback(hash) {
|
|
64
69
|
const changes = this.store.readAll();
|
|
@@ -71,8 +76,34 @@ export class PromptVcs {
|
|
|
71
76
|
this.recordChange(fullPath, change.newContent, change.oldContent, 'user');
|
|
72
77
|
return true;
|
|
73
78
|
}
|
|
79
|
+
/** Get stats per file. */
|
|
80
|
+
fileStats() {
|
|
81
|
+
const changes = this.store.readAll();
|
|
82
|
+
const byFile = new Map();
|
|
83
|
+
for (const c of changes) {
|
|
84
|
+
const f = byFile.get(c.file) ?? { changes: 0, lastChanged: 0 };
|
|
85
|
+
f.changes++;
|
|
86
|
+
f.lastChanged = Math.max(f.lastChanged, c.timestamp);
|
|
87
|
+
byFile.set(c.file, f);
|
|
88
|
+
}
|
|
89
|
+
return [...byFile.entries()].map(([file, v]) => {
|
|
90
|
+
const fullPath = join(this.projectDir, file);
|
|
91
|
+
let currentSize = 0;
|
|
92
|
+
try {
|
|
93
|
+
currentSize = existsSync(fullPath) ? readFileSync(fullPath, 'utf8').length : 0;
|
|
94
|
+
}
|
|
95
|
+
catch { /* */ }
|
|
96
|
+
return { file, ...v, currentSize };
|
|
97
|
+
}).sort((a, b) => b.lastChanged - a.lastChanged);
|
|
98
|
+
}
|
|
74
99
|
panel() {
|
|
75
|
-
|
|
100
|
+
const changes = this.store.readAll();
|
|
101
|
+
return {
|
|
102
|
+
timeline: this.timeline(),
|
|
103
|
+
files: this.fileStats(),
|
|
104
|
+
totalChanges: changes.length,
|
|
105
|
+
watchedFiles: WATCHED_FILES,
|
|
106
|
+
};
|
|
76
107
|
}
|
|
77
108
|
static get watchedFiles() {
|
|
78
109
|
return WATCHED_FILES;
|
package/lib/vcs.web.js
CHANGED
|
@@ -42,7 +42,21 @@ var zh = {
|
|
|
42
42
|
"refresh": "\u5237\u65B0",
|
|
43
43
|
"loading": "\u6B63\u5728\u52A0\u8F7D\u2026",
|
|
44
44
|
"failed": "\u52A0\u8F7D\u5931\u8D25",
|
|
45
|
-
"retry": "\u91CD\u8BD5"
|
|
45
|
+
"retry": "\u91CD\u8BD5",
|
|
46
|
+
"totalChanges": "\u603B\u53D8\u66F4\u6570",
|
|
47
|
+
"filesTracked": "\u8DDF\u8E2A\u6587\u4EF6\u6570",
|
|
48
|
+
"watched": "\u76D1\u63A7\u6587\u4EF6",
|
|
49
|
+
"files": "\u6587\u4EF6",
|
|
50
|
+
"changes": "\u53D8\u66F4\u6B21\u6570",
|
|
51
|
+
"lastChanged": "\u6700\u540E\u53D8\u66F4",
|
|
52
|
+
"size": "\u5927\u5C0F",
|
|
53
|
+
"timeline": "\u65F6\u95F4\u7EBF",
|
|
54
|
+
"diff": "\u5DEE\u5F02",
|
|
55
|
+
"before": "\u53D8\u66F4\u524D",
|
|
56
|
+
"after": "\u53D8\u66F4\u540E",
|
|
57
|
+
"rollback": "\u56DE\u6EDA",
|
|
58
|
+
"confirmRollback": "\u786E\u5B9A\u56DE\u6EDA\u5230\u6B64\u53D8\u66F4\u4E4B\u524D\u7684\u72B6\u6001\uFF1F",
|
|
59
|
+
"notFound": "\u672A\u627E\u5230\u53D8\u66F4\u8BB0\u5F55"
|
|
46
60
|
};
|
|
47
61
|
var en = {
|
|
48
62
|
"nav": "Prompt VCS",
|
|
@@ -57,45 +71,130 @@ var en = {
|
|
|
57
71
|
"refresh": "Refresh",
|
|
58
72
|
"loading": "Loading\u2026",
|
|
59
73
|
"failed": "Failed to load",
|
|
60
|
-
"retry": "Retry"
|
|
74
|
+
"retry": "Retry",
|
|
75
|
+
"totalChanges": "Total Changes",
|
|
76
|
+
"filesTracked": "Files Tracked",
|
|
77
|
+
"watched": "Watched",
|
|
78
|
+
"files": "Files",
|
|
79
|
+
"changes": "Changes",
|
|
80
|
+
"lastChanged": "Last Changed",
|
|
81
|
+
"size": "Size",
|
|
82
|
+
"timeline": "Timeline",
|
|
83
|
+
"diff": "Diff",
|
|
84
|
+
"before": "Before",
|
|
85
|
+
"after": "After",
|
|
86
|
+
"rollback": "Rollback",
|
|
87
|
+
"confirmRollback": "Rollback to before this change?",
|
|
88
|
+
"notFound": "Change not found"
|
|
61
89
|
};
|
|
62
90
|
|
|
63
91
|
// src/client/view.tsx
|
|
64
92
|
var import_react = require("react");
|
|
65
93
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
66
94
|
var PANEL_PATH = "/api/vcs.panel";
|
|
67
|
-
var
|
|
95
|
+
var CHANGE_PATH = "/api/vcs.change";
|
|
96
|
+
var ROLLBACK_PATH = "/api/vcs.rollback";
|
|
97
|
+
var wrap = { display: "flex", flexDirection: "column", gap: 12, maxWidth: 820, fontFamily: "inherit" };
|
|
68
98
|
var head = { display: "flex", alignItems: "baseline", gap: 12, flexWrap: "wrap" };
|
|
69
99
|
var muted = { fontSize: 12, opacity: 0.75 };
|
|
70
100
|
var table = { borderCollapse: "collapse", width: "100%" };
|
|
71
101
|
var th = { textAlign: "left", padding: "4px 10px 4px 0", fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: "0.5px solid rgba(128,128,128,0.4)" };
|
|
72
102
|
var td = { padding: "6px 10px 6px 0", fontSize: 13, borderBottom: "0.5px solid rgba(128,128,128,0.18)" };
|
|
103
|
+
var card = { padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(128,128,128,0.2)", fontSize: 13 };
|
|
104
|
+
var btn = { fontSize: 12, cursor: "pointer", padding: "3px 10px", borderRadius: 4, border: "0.5px solid rgba(128,128,128,0.4)" };
|
|
105
|
+
var dangerBtn = { ...btn, color: "#e55", borderColor: "#e55" };
|
|
106
|
+
var preBlock = { whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11, maxHeight: 200, overflow: "auto", padding: 8, borderRadius: 4, background: "rgba(128,128,128,0.06)", border: "1px solid rgba(128,128,128,0.15)" };
|
|
107
|
+
var clickRow = { cursor: "pointer" };
|
|
108
|
+
var statRow = { display: "flex", gap: 12, flexWrap: "wrap" };
|
|
109
|
+
var statBox = { ...card, flex: 1, minWidth: 100, textAlign: "center" };
|
|
73
110
|
function usePanel() {
|
|
74
111
|
const [state, setState] = (0, import_react.useState)({ payload: null, error: null });
|
|
75
112
|
const [tick, setTick] = (0, import_react.useState)(0);
|
|
76
|
-
const reload = (0, import_react.useCallback)(() => setTick((
|
|
113
|
+
const reload = (0, import_react.useCallback)(() => setTick((v) => v + 1), []);
|
|
77
114
|
(0, import_react.useEffect)(() => {
|
|
78
|
-
const
|
|
79
|
-
setState((
|
|
80
|
-
fetch(PANEL_PATH, { signal:
|
|
81
|
-
if (!
|
|
82
|
-
return
|
|
115
|
+
const c = new AbortController();
|
|
116
|
+
setState((p) => ({ ...p, error: null }));
|
|
117
|
+
fetch(PANEL_PATH, { signal: c.signal }).then(async (r) => {
|
|
118
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
119
|
+
return r.json();
|
|
83
120
|
}).then((payload) => {
|
|
84
|
-
if (!
|
|
85
|
-
}).catch((
|
|
86
|
-
if (
|
|
87
|
-
setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
|
|
121
|
+
if (!c.signal.aborted) setState({ payload, error: null });
|
|
122
|
+
}).catch((e) => {
|
|
123
|
+
if (!c.signal.aborted) setState({ payload: null, error: e instanceof Error ? e.message : String(e) });
|
|
88
124
|
});
|
|
89
|
-
return () =>
|
|
125
|
+
return () => c.abort();
|
|
90
126
|
}, [tick]);
|
|
91
127
|
return { ...state, reload };
|
|
92
128
|
}
|
|
129
|
+
function ChangeDetail({ hash, t, onRollback }) {
|
|
130
|
+
const [change, setChange] = (0, import_react.useState)(null);
|
|
131
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
132
|
+
const [rolling, setRolling] = (0, import_react.useState)(false);
|
|
133
|
+
const load = (0, import_react.useCallback)(async () => {
|
|
134
|
+
setLoading(true);
|
|
135
|
+
try {
|
|
136
|
+
const r = await fetch(`${CHANGE_PATH}?hash=${hash}`);
|
|
137
|
+
if (r.ok) setChange(await r.json());
|
|
138
|
+
} finally {
|
|
139
|
+
setLoading(false);
|
|
140
|
+
}
|
|
141
|
+
}, [hash]);
|
|
142
|
+
(0, import_react.useEffect)(() => {
|
|
143
|
+
load();
|
|
144
|
+
}, [load]);
|
|
145
|
+
const handleRollback = (0, import_react.useCallback)(async () => {
|
|
146
|
+
if (!confirm(t("confirmRollback"))) return;
|
|
147
|
+
setRolling(true);
|
|
148
|
+
try {
|
|
149
|
+
await fetch(ROLLBACK_PATH, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "content-type": "application/json" },
|
|
152
|
+
body: JSON.stringify({ hash })
|
|
153
|
+
});
|
|
154
|
+
onRollback();
|
|
155
|
+
} finally {
|
|
156
|
+
setRolling(false);
|
|
157
|
+
}
|
|
158
|
+
}, [hash, t, onRollback]);
|
|
159
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: t("loading") });
|
|
160
|
+
if (!change) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, children: t("notFound") });
|
|
161
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...card, marginTop: 4 }, children: [
|
|
162
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }, children: [
|
|
163
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: change.file }),
|
|
164
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: dangerBtn, disabled: rolling, onClick: handleRollback, children: rolling ? "\u2026" : `\u21A9 ${t("rollback")}` })
|
|
165
|
+
] }),
|
|
166
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 8 }, children: [
|
|
167
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12 }, children: t("diff") }),
|
|
168
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { style: preBlock, children: change.diff })
|
|
169
|
+
] }),
|
|
170
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [
|
|
171
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 250 }, children: [
|
|
172
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12, color: "#e55" }, children: t("before") }),
|
|
173
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("pre", { style: { ...preBlock, maxHeight: 150 }, children: [
|
|
174
|
+
change.oldContent.slice(0, 2e3),
|
|
175
|
+
change.oldContent.length > 2e3 ? "\n\u2026" : ""
|
|
176
|
+
] })
|
|
177
|
+
] }),
|
|
178
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 250 }, children: [
|
|
179
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { style: { fontSize: 12, color: "#4a4" }, children: t("after") }),
|
|
180
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("pre", { style: { ...preBlock, maxHeight: 150 }, children: [
|
|
181
|
+
change.newContent.slice(0, 2e3),
|
|
182
|
+
change.newContent.length > 2e3 ? "\n\u2026" : ""
|
|
183
|
+
] })
|
|
184
|
+
] })
|
|
185
|
+
] })
|
|
186
|
+
] });
|
|
187
|
+
}
|
|
93
188
|
function PromptVcsPanel({ t }) {
|
|
94
189
|
const { payload, error, reload } = usePanel();
|
|
190
|
+
const [expandedHash, setExpandedHash] = (0, import_react.useState)(null);
|
|
95
191
|
const header = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: head, children: [
|
|
96
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
192
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
193
|
+
"\u{1F4DD} ",
|
|
194
|
+
t("title")
|
|
195
|
+
] }),
|
|
97
196
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
|
|
98
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button",
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", style: btn, onClick: reload, children: t("refresh") })
|
|
99
198
|
] });
|
|
100
199
|
if (error !== null) {
|
|
101
200
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
@@ -105,35 +204,86 @@ function PromptVcsPanel({ t }) {
|
|
|
105
204
|
": ",
|
|
106
205
|
error
|
|
107
206
|
] }),
|
|
108
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { alignSelf: "flex-start"
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: reload, style: { ...btn, alignSelf: "flex-start" }, children: t("retry") })
|
|
109
208
|
] });
|
|
110
209
|
}
|
|
111
210
|
if (payload === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: muted, "aria-live": "polite", children: t("loading") });
|
|
112
211
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wrap, children: [
|
|
113
212
|
header,
|
|
114
|
-
|
|
115
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
116
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
117
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
121
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
122
|
-
] })
|
|
123
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
124
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
125
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
213
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statRow, children: [
|
|
214
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 18, fontWeight: 700 }, children: payload.totalChanges }),
|
|
216
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("totalChanges") })
|
|
217
|
+
] }),
|
|
218
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 18, fontWeight: 700 }, children: payload.files.length }),
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("filesTracked") })
|
|
221
|
+
] }),
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: statBox, children: [
|
|
223
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 14, fontWeight: 600 }, children: payload.watchedFiles.join(", ") }),
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: muted, children: t("watched") })
|
|
225
|
+
] })
|
|
226
|
+
] }),
|
|
227
|
+
payload.files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
229
|
+
"\u{1F4C1} ",
|
|
230
|
+
t("files")
|
|
231
|
+
] }),
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: { ...table, marginTop: 4 }, children: [
|
|
233
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
234
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("file") }),
|
|
235
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("changes") }),
|
|
236
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("lastChanged") }),
|
|
237
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("size") })
|
|
238
|
+
] }) }),
|
|
239
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: payload.files.map((f) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
240
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: f.file }) }),
|
|
241
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: f.changes }),
|
|
242
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: new Date(f.lastChanged).toLocaleString() }),
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: td, children: [
|
|
244
|
+
f.currentSize,
|
|
245
|
+
"B"
|
|
246
|
+
] })
|
|
247
|
+
] }, f.file)) })
|
|
248
|
+
] })
|
|
249
|
+
] }),
|
|
250
|
+
payload.timeline.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t("empty") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
251
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
252
|
+
"\u{1F552} ",
|
|
253
|
+
t("timeline")
|
|
254
|
+
] }),
|
|
255
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", { style: { ...table, marginTop: 4 }, children: [
|
|
256
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
|
|
257
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th }),
|
|
258
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("hash") }),
|
|
259
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("date") }),
|
|
260
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("by") }),
|
|
261
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("file") }),
|
|
262
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("added") }),
|
|
263
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", { style: th, children: t("removed") })
|
|
264
|
+
] }) }),
|
|
265
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: payload.timeline.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
266
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { style: clickRow, onClick: () => setExpandedHash(expandedHash === row.hash ? null : row.hash), children: [
|
|
267
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: expandedHash === row.hash ? "\u25BC" : "\u25B6" }),
|
|
268
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: { fontSize: 11 }, children: row.hash }) }),
|
|
269
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: new Date(row.timestamp).toLocaleString() }),
|
|
270
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: row.changedBy }),
|
|
271
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { style: td, children: row.file }),
|
|
272
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: { ...td, color: "#4a4" }, children: [
|
|
273
|
+
"+",
|
|
274
|
+
row.addedLines
|
|
275
|
+
] }),
|
|
276
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", { style: { ...td, color: "#e55" }, children: [
|
|
277
|
+
"-",
|
|
278
|
+
row.removedLines
|
|
279
|
+
] })
|
|
280
|
+
] }, row.hash),
|
|
281
|
+
expandedHash === row.hash && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { colSpan: 7, style: { padding: "4px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChangeDetail, { hash: row.hash, t, onRollback: () => {
|
|
282
|
+
setExpandedHash(null);
|
|
283
|
+
reload();
|
|
284
|
+
} }) }) }, `${row.hash}-detail`)
|
|
285
|
+
] })) })
|
|
286
|
+
] })
|
|
137
287
|
] })
|
|
138
288
|
] });
|
|
139
289
|
}
|
package/lib/vcs.web.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/client/index.tsx", "../src/client/locales.ts", "../src/client/view.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Browser half of dsh-prompt-vcs: its page inside Settings.\n *\n * Two services are injected by their runtime identity, not by the packages that provide\n * them: the settings shell is `slots`, the dictionary registry is `locale`. The label and\n * every string inside the component are translated through this plugin's own namespace,\n * which is why the page follows the UI language instead of the plugin author's.\n *\n * @module client\n */\n\nimport type { ComponentType } from 'react'\n\nimport { NS, en, zh } from './locales.js'\nimport { PromptVcsPanel } from './view.js'\n\nexport const inject = ['slots', 'locale']\n\ninterface SlotsService {\n inject(name: string, register: () => void): void\n register(\n options: { name: string; id: string; order: number; label: () => string; locale: string },\n component: ComponentType<{ t: (key: string, params?: Record<string, unknown>) => string }>,\n ): unknown\n}\n\ninterface ClientContext {\n slots: SlotsService\n locale: {\n register(ns: string, dicts: { zh: unknown; en: unknown }): () => void\n bind(ns: string): (key: string, params?: Record<string, unknown>) => string\n }\n effect(callback: () => unknown, label?: string): unknown\n}\n\nexport function apply(ctx: ClientContext): void {\n // `zh` is the key-set source of truth, matching the official client plugins.\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-prompt-vcs: dictionaries')\n\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n {\n name: 'settings.section',\n id: 'prompt-vcs',\n order: 44,\n label: () => ctx.locale.bind(NS)('nav'),\n locale: NS,\n },\n PromptVcsPanel,\n ))\n}\n", "/**\n * Dictionaries for the Prompt VCS page.\n *\n * `zh` is the key-set source of truth, as in the official client plugins, and `en` is\n * typed against it: a key translated in one language but not the other fails the build\n * instead of silently rendering the raw key.\n *\n * @module client/locales\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'promptVcs'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'nav': '\u63D0\u793A\u8BCD\u7248\u672C',\n 'title': '\u63D0\u793A\u8BCD\u7248\u672C',\n 'hash': '\u7248\u672C\u53F7',\n 'date': '\u65F6\u95F4',\n 'by': '\u4FEE\u6539\u65B9',\n 'file': '\u6587\u4EF6',\n 'added': '\u65B0\u589E',\n 'removed': '\u5220\u9664',\n 'empty': '\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u6307\u4EE4\u6539\u52A8\u3002',\n 'refresh': '\u5237\u65B0',\n 'loading': '\u6B63\u5728\u52A0\u8F7D\u2026',\n 'failed': '\u52A0\u8F7D\u5931\u8D25',\n 'retry': '\u91CD\u8BD5',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: typeof zh = {\n 'nav': 'Prompt VCS',\n 'title': 'Prompt VCS',\n 'hash': 'Hash',\n 'date': 'Date',\n 'by': 'Changed by',\n 'file': 'File',\n 'added': 'Added',\n 'removed': 'Removed',\n 'empty': 'No instruction changes recorded yet.',\n 'refresh': 'Refresh',\n 'loading': 'Loading\u2026',\n 'failed': 'Failed to load',\n 'retry': 'Retry',\n}\n", "/**\n * Pure rendering half of the promptVcs page.\n *\n * Separate from `index.tsx` so a static render can assert in Node what the page draws \u2014\n * the shipped bundle is a loader factory only a browser can run. Every user-visible\n * string comes from the `t` seat the renderer binds from this plugin's namespace, so the\n * page follows the UI language; no copy is hardcoded here.\n *\n * @module client/view\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { CSSProperties } from 'react'\n\nimport type { PanelPayload } from '../types.js'\n\n/** The translate seat the renderer binds from this plugin's locale namespace. */\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\nexport interface PanelProps {\n /** Bound translate function for this plugin's namespace. */\n t: Translate\n}\n\n/** Panel route registered by the host half on the web connection. */\nconst PANEL_PATH = \"/api/vcs.panel\"\n\nconst wrap: CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 760, fontFamily: 'inherit' }\nconst head: CSSProperties = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }\nconst muted: CSSProperties = { fontSize: 12, opacity: 0.75 }\nconst table: CSSProperties = { borderCollapse: 'collapse', width: '100%' }\nconst th: CSSProperties = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' }\nconst td: CSSProperties = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' }\n\ninterface PanelState {\n payload: PanelPayload | null\n error: string | null\n}\n\n/** Fetch the host panel payload; `reload` re-runs the request. */\nexport function usePanel(): PanelState & { reload: () => void } {\n const [state, setState] = useState<PanelState>({ payload: null, error: null })\n const [tick, setTick] = useState(0)\n const reload = useCallback(() => setTick((value) => value + 1), [])\n\n useEffect(() => {\n const controller = new AbortController()\n setState((previous) => ({ ...previous, error: null }))\n fetch(PANEL_PATH, { signal: controller.signal })\n .then(async (response) => {\n if (!response.ok) throw new Error(String(response.status))\n return response.json() as Promise<PanelPayload>\n })\n .then((payload) => {\n if (!controller.signal.aborted) setState({ payload, error: null })\n })\n .catch((cause: unknown) => {\n if (controller.signal.aborted) return\n setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) })\n })\n return () => controller.abort()\n }, [tick])\n\n return { ...state, reload }\n}\n\nexport function PromptVcsPanel({ t }: PanelProps) {\n const { payload, error, reload } = usePanel()\n const header = (\n <header style={head}>\n <strong style={{ fontSize: 13 }}>{t('title')}</strong>\n <span style={{ flex: 1 }} />\n <button type=\"button\" onClick={reload} style={{ fontSize: 12 }}>{t('refresh')}</button>\n </header>\n )\n if (error !== null) {\n return (\n <div style={wrap}>\n {header}\n <p role=\"alert\" style={{ margin: 0, fontSize: 13 }}>{t('failed')}: {error}</p>\n <button type=\"button\" onClick={reload} style={{ alignSelf: 'flex-start', fontSize: 12 }}>{t('retry')}</button>\n </div>\n )\n }\n if (payload === null) return <p style={muted} aria-live=\"polite\">{t('loading')}</p>\n return (\n <div style={wrap}>\n {header}\n {payload.timeline.length === 0 ? (\n <p style={{ margin: 0, fontSize: 13, opacity: 0.8 }}>{t('empty')}</p>\n ) : (\n <table style={table}>\n <thead>\n <tr>\n <th style={th}>{t('hash')}</th><th style={th}>{t('date')}</th><th style={th}>{t('by')}</th>\n <th style={th}>{t('file')}</th><th style={th}>{t('added')}</th><th style={th}>{t('removed')}</th>\n </tr>\n </thead>\n <tbody>\n {payload.timeline.map((row) => (\n <tr key={row.hash}>\n <td style={td}><code style={{ fontSize: 11 }}>{row.hash}</code></td>\n <td style={td}>{new Date(row.timestamp).toLocaleString()}</td>\n <td style={td}>{row.changedBy}</td>\n <td style={td}>{row.file}</td>\n <td style={td}>+{row.addedLines}</td>\n <td style={td}>-{row.removedLines}</td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWO,IAAM,KAAK;AAGX,IAAM,KAAK;AAAA,EAChB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;
|
|
4
|
+
"sourcesContent": ["/**\n * Browser half of dsh-prompt-vcs: its page inside Settings.\n *\n * Two services are injected by their runtime identity, not by the packages that provide\n * them: the settings shell is `slots`, the dictionary registry is `locale`. The label and\n * every string inside the component are translated through this plugin's own namespace,\n * which is why the page follows the UI language instead of the plugin author's.\n *\n * @module client\n */\n\nimport type { ComponentType } from 'react'\n\nimport { NS, en, zh } from './locales.js'\nimport { PromptVcsPanel } from './view.js'\n\nexport const inject = ['slots', 'locale']\n\ninterface SlotsService {\n inject(name: string, register: () => void): void\n register(\n options: { name: string; id: string; order: number; label: () => string; locale: string },\n component: ComponentType<{ t: (key: string, params?: Record<string, unknown>) => string }>,\n ): unknown\n}\n\ninterface ClientContext {\n slots: SlotsService\n locale: {\n register(ns: string, dicts: { zh: unknown; en: unknown }): () => void\n bind(ns: string): (key: string, params?: Record<string, unknown>) => string\n }\n effect(callback: () => unknown, label?: string): unknown\n}\n\nexport function apply(ctx: ClientContext): void {\n // `zh` is the key-set source of truth, matching the official client plugins.\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-prompt-vcs: dictionaries')\n\n ctx.slots.inject('settings.section', () => ctx.slots.register(\n {\n name: 'settings.section',\n id: 'prompt-vcs',\n order: 44,\n label: () => ctx.locale.bind(NS)('nav'),\n locale: NS,\n },\n PromptVcsPanel,\n ))\n}\n", "/**\n * Dictionaries for the Prompt VCS page.\n *\n * `zh` is the key-set source of truth, as in the official client plugins, and `en` is\n * typed against it: a key translated in one language but not the other fails the build\n * instead of silently rendering the raw key.\n *\n * @module client/locales\n */\n\n/** Dictionary namespace owned by this plugin. */\nexport const NS = 'promptVcs'\n\n/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'nav': '\u63D0\u793A\u8BCD\u7248\u672C',\n 'title': '\u63D0\u793A\u8BCD\u7248\u672C',\n 'hash': '\u7248\u672C\u53F7',\n 'date': '\u65F6\u95F4',\n 'by': '\u4FEE\u6539\u65B9',\n 'file': '\u6587\u4EF6',\n 'added': '\u65B0\u589E',\n 'removed': '\u5220\u9664',\n 'empty': '\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u6307\u4EE4\u6539\u52A8\u3002',\n 'refresh': '\u5237\u65B0',\n 'loading': '\u6B63\u5728\u52A0\u8F7D\u2026',\n 'failed': '\u52A0\u8F7D\u5931\u8D25',\n 'retry': '\u91CD\u8BD5',\n 'totalChanges': '\u603B\u53D8\u66F4\u6570',\n 'filesTracked': '\u8DDF\u8E2A\u6587\u4EF6\u6570',\n 'watched': '\u76D1\u63A7\u6587\u4EF6',\n 'files': '\u6587\u4EF6',\n 'changes': '\u53D8\u66F4\u6B21\u6570',\n 'lastChanged': '\u6700\u540E\u53D8\u66F4',\n 'size': '\u5927\u5C0F',\n 'timeline': '\u65F6\u95F4\u7EBF',\n 'diff': '\u5DEE\u5F02',\n 'before': '\u53D8\u66F4\u524D',\n 'after': '\u53D8\u66F4\u540E',\n 'rollback': '\u56DE\u6EDA',\n 'confirmRollback': '\u786E\u5B9A\u56DE\u6EDA\u5230\u6B64\u53D8\u66F4\u4E4B\u524D\u7684\u72B6\u6001\uFF1F',\n 'notFound': '\u672A\u627E\u5230\u53D8\u66F4\u8BB0\u5F55',\n}\n\n/** English dictionary, checked complete against the zh key set. */\nexport const en: typeof zh = {\n 'nav': 'Prompt VCS',\n 'title': 'Prompt VCS',\n 'hash': 'Hash',\n 'date': 'Date',\n 'by': 'Changed by',\n 'file': 'File',\n 'added': 'Added',\n 'removed': 'Removed',\n 'empty': 'No instruction changes recorded yet.',\n 'refresh': 'Refresh',\n 'loading': 'Loading\u2026',\n 'failed': 'Failed to load',\n 'retry': 'Retry',\n 'totalChanges': 'Total Changes',\n 'filesTracked': 'Files Tracked',\n 'watched': 'Watched',\n 'files': 'Files',\n 'changes': 'Changes',\n 'lastChanged': 'Last Changed',\n 'size': 'Size',\n 'timeline': 'Timeline',\n 'diff': 'Diff',\n 'before': 'Before',\n 'after': 'After',\n 'rollback': 'Rollback',\n 'confirmRollback': 'Rollback to before this change?',\n 'notFound': 'Change not found',\n}\n", "/**\n * Pure rendering half of the promptVcs page.\n *\n * Separate from `index.tsx` so a static render can assert in Node what the page draws \u2014\n * the shipped bundle is a loader factory only a browser can run. Every user-visible\n * string comes from the `t` seat the renderer binds from this plugin's namespace, so the\n * page follows the UI language; no copy is hardcoded here.\n *\n * @module client/view\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { CSSProperties } from 'react'\n\nimport type { PanelPayload, Change } from '../types.js'\n\nexport type Translate = (key: string, params?: Record<string, unknown>) => string\n\nexport interface PanelProps {\n t: Translate\n}\n\nconst PANEL_PATH = '/api/vcs.panel'\nconst CHANGE_PATH = '/api/vcs.change'\nconst ROLLBACK_PATH = '/api/vcs.rollback'\n\nconst wrap: CSSProperties = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 820, fontFamily: 'inherit' }\nconst head: CSSProperties = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }\nconst muted: CSSProperties = { fontSize: 12, opacity: 0.75 }\nconst table: CSSProperties = { borderCollapse: 'collapse', width: '100%' }\nconst th: CSSProperties = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' }\nconst td: CSSProperties = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' }\nconst card: CSSProperties = { padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(128,128,128,0.2)', fontSize: 13 }\nconst btn: CSSProperties = { fontSize: 12, cursor: 'pointer', padding: '3px 10px', borderRadius: 4, border: '0.5px solid rgba(128,128,128,0.4)' }\nconst dangerBtn: CSSProperties = { ...btn, color: '#e55', borderColor: '#e55' }\nconst preBlock: CSSProperties = { whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 11, maxHeight: 200, overflow: 'auto', padding: 8, borderRadius: 4, background: 'rgba(128,128,128,0.06)', border: '1px solid rgba(128,128,128,0.15)' }\nconst clickRow: CSSProperties = { cursor: 'pointer' }\nconst statRow: CSSProperties = { display: 'flex', gap: 12, flexWrap: 'wrap' }\nconst statBox: CSSProperties = { ...card, flex: 1, minWidth: 100, textAlign: 'center' as const }\n\ninterface PanelState {\n payload: PanelPayload | null\n error: string | null\n}\n\nexport function usePanel(): PanelState & { reload: () => void } {\n const [state, setState] = useState<PanelState>({ payload: null, error: null })\n const [tick, setTick] = useState(0)\n const reload = useCallback(() => setTick((v) => v + 1), [])\n useEffect(() => {\n const c = new AbortController()\n setState((p) => ({ ...p, error: null }))\n fetch(PANEL_PATH, { signal: c.signal })\n .then(async (r) => { if (!r.ok) throw new Error(String(r.status)); return r.json() as Promise<PanelPayload> })\n .then((payload) => { if (!c.signal.aborted) setState({ payload, error: null }) })\n .catch((e: unknown) => { if (!c.signal.aborted) setState({ payload: null, error: e instanceof Error ? e.message : String(e) }) })\n return () => c.abort()\n }, [tick])\n return { ...state, reload }\n}\n\n// --- Change detail expansion with diff and rollback ---\nfunction ChangeDetail({ hash, t, onRollback }: { hash: string; t: Translate; onRollback: () => void }) {\n const [change, setChange] = useState<Change | null>(null)\n const [loading, setLoading] = useState(false)\n const [rolling, setRolling] = useState(false)\n\n const load = useCallback(async () => {\n setLoading(true)\n try {\n const r = await fetch(`${CHANGE_PATH}?hash=${hash}`)\n if (r.ok) setChange(await r.json() as Change)\n } finally { setLoading(false) }\n }, [hash])\n\n useEffect(() => { load() }, [load])\n\n const handleRollback = useCallback(async () => {\n if (!confirm(t('confirmRollback'))) return\n setRolling(true)\n try {\n await fetch(ROLLBACK_PATH, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ hash }),\n })\n onRollback()\n } finally { setRolling(false) }\n }, [hash, t, onRollback])\n\n if (loading) return <p style={muted}>{t('loading')}</p>\n if (!change) return <p style={muted}>{t('notFound')}</p>\n return (\n <div style={{ ...card, marginTop: 4 }}>\n <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>\n <strong>{change.file}</strong>\n <button type=\"button\" style={dangerBtn} disabled={rolling} onClick={handleRollback}>\n {rolling ? '\u2026' : `\u21A9 ${t('rollback')}`}\n </button>\n </div>\n {/* Diff display */}\n <div style={{ marginBottom: 8 }}>\n <strong style={{ fontSize: 12 }}>{t('diff')}</strong>\n <pre style={preBlock}>{change.diff}</pre>\n </div>\n {/* Old / New content */}\n <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>\n <div style={{ flex: 1, minWidth: 250 }}>\n <strong style={{ fontSize: 12, color: '#e55' }}>{t('before')}</strong>\n <pre style={{ ...preBlock, maxHeight: 150 }}>{change.oldContent.slice(0, 2000)}{change.oldContent.length > 2000 ? '\\n\u2026' : ''}</pre>\n </div>\n <div style={{ flex: 1, minWidth: 250 }}>\n <strong style={{ fontSize: 12, color: '#4a4' }}>{t('after')}</strong>\n <pre style={{ ...preBlock, maxHeight: 150 }}>{change.newContent.slice(0, 2000)}{change.newContent.length > 2000 ? '\\n\u2026' : ''}</pre>\n </div>\n </div>\n </div>\n )\n}\n\nexport function PromptVcsPanel({ t }: PanelProps) {\n const { payload, error, reload } = usePanel()\n const [expandedHash, setExpandedHash] = useState<string | null>(null)\n\n const header = (\n <header style={head}>\n <strong style={{ fontSize: 13 }}>\uD83D\uDCDD {t('title')}</strong>\n <span style={{ flex: 1 }} />\n <button type=\"button\" style={btn} onClick={reload}>{t('refresh')}</button>\n </header>\n )\n if (error !== null) {\n return (\n <div style={wrap}>\n {header}\n <p role=\"alert\" style={{ margin: 0, fontSize: 13 }}>{t('failed')}: {error}</p>\n <button type=\"button\" onClick={reload} style={{ ...btn, alignSelf: 'flex-start' }}>{t('retry')}</button>\n </div>\n )\n }\n if (payload === null) return <p style={muted} aria-live=\"polite\">{t('loading')}</p>\n return (\n <div style={wrap}>\n {header}\n\n {/* Overview */}\n <div style={statRow}>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{payload.totalChanges}</div><div style={muted}>{t('totalChanges')}</div></div>\n <div style={statBox}><div style={{ fontSize: 18, fontWeight: 700 }}>{payload.files.length}</div><div style={muted}>{t('filesTracked')}</div></div>\n <div style={statBox}><div style={{ fontSize: 14, fontWeight: 600 }}>{payload.watchedFiles.join(', ')}</div><div style={muted}>{t('watched')}</div></div>\n </div>\n\n {/* File stats */}\n {payload.files.length > 0 && (\n <div>\n <strong style={{ fontSize: 13 }}>\uD83D\uDCC1 {t('files')}</strong>\n <table style={{ ...table, marginTop: 4 }}>\n <thead>\n <tr><th style={th}>{t('file')}</th><th style={th}>{t('changes')}</th><th style={th}>{t('lastChanged')}</th><th style={th}>{t('size')}</th></tr>\n </thead>\n <tbody>\n {payload.files.map((f) => (\n <tr key={f.file}>\n <td style={td}><code style={{ fontSize: 11 }}>{f.file}</code></td>\n <td style={td}>{f.changes}</td>\n <td style={td}>{new Date(f.lastChanged).toLocaleString()}</td>\n <td style={td}>{f.currentSize}B</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n\n {/* Timeline (expandable with diff + rollback) */}\n {payload.timeline.length === 0 ? (\n <p style={{ margin: 0, fontSize: 13, opacity: 0.8 }}>{t('empty')}</p>\n ) : (\n <div>\n <strong style={{ fontSize: 13 }}>\uD83D\uDD52 {t('timeline')}</strong>\n <table style={{ ...table, marginTop: 4 }}>\n <thead>\n <tr>\n <th style={th} /><th style={th}>{t('hash')}</th><th style={th}>{t('date')}</th><th style={th}>{t('by')}</th>\n <th style={th}>{t('file')}</th><th style={th}>{t('added')}</th><th style={th}>{t('removed')}</th>\n </tr>\n </thead>\n <tbody>\n {payload.timeline.map((row) => (\n <>\n <tr key={row.hash} style={clickRow} onClick={() => setExpandedHash(expandedHash === row.hash ? null : row.hash)}>\n <td style={td}>{expandedHash === row.hash ? '\u25BC' : '\u25B6'}</td>\n <td style={td}><code style={{ fontSize: 11 }}>{row.hash}</code></td>\n <td style={td}>{new Date(row.timestamp).toLocaleString()}</td>\n <td style={td}>{row.changedBy}</td>\n <td style={td}>{row.file}</td>\n <td style={{ ...td, color: '#4a4' }}>+{row.addedLines}</td>\n <td style={{ ...td, color: '#e55' }}>-{row.removedLines}</td>\n </tr>\n {expandedHash === row.hash && (\n <tr key={`${row.hash}-detail`}>\n <td colSpan={7} style={{ padding: '4px 0' }}>\n <ChangeDetail hash={row.hash} t={t} onRollback={() => { setExpandedHash(null); reload() }} />\n </td>\n </tr>\n )}\n </>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|