@young1lin/dsh-ui-gitworkbench 0.1.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/AGENTS.md +70 -0
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/README_EN.md +74 -0
- package/cordis.patch.yml +7 -0
- package/lib/atomic-json.js +48 -0
- package/lib/client.js +15297 -0
- package/lib/commit-cache.js +68 -0
- package/lib/git-log.js +79 -0
- package/lib/git-ops.js +409 -0
- package/lib/index.js +1143 -0
- package/lib/style-store.js +123 -0
- package/lib/worktree.js +112 -0
- package/package.json +86 -0
- package/scripts/install.ps1 +240 -0
- package/scripts/install.sh +231 -0
- package/src/atomic-json.ts +55 -0
- package/src/client/GitWorkbenchPanel.module.css +1512 -0
- package/src/client/GitWorkbenchPanel.tsx +3446 -0
- package/src/client/commit-graph.ts +140 -0
- package/src/client/diff-model.ts +193 -0
- package/src/client/highlight.ts +257 -0
- package/src/client/index.ts +198 -0
- package/src/client/locales.ts +270 -0
- package/src/client/op-feedback.ts +65 -0
- package/src/client/stage-tree.ts +178 -0
- package/src/client/themes.ts +181 -0
- package/src/client/worktree-view.ts +193 -0
- package/src/commit-cache.ts +69 -0
- package/src/git-log.ts +92 -0
- package/src/git-ops.ts +490 -0
- package/src/index.ts +1172 -0
- package/src/style-store.ts +144 -0
- package/src/types/dsh-client-shim.d.ts +100 -0
- package/src/types/dsh-shim.d.ts +77 -0
- package/src/worktree.ts +142 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of @young1lin/dsh-ui-gitworkbench.
|
|
3
|
+
*
|
|
4
|
+
* Contributes one entry to the `conversation.session.header.actions` list slot:
|
|
5
|
+
* a compact git-workbench chip. Clicking it opens a right-side drawer with the
|
|
6
|
+
* file list and per-file diff. The data is fetched from the host's
|
|
7
|
+
* `gitWorkbench/stats` Remote method through the generic connection RPC channel;
|
|
8
|
+
* `gitWorkbench/sessionWorktree` reports the session's worktree binding so the
|
|
9
|
+
* chip can badge it and the drawer can switch between bound/main sources;
|
|
10
|
+
* `gitWorkbench/styleGet` and `styleSet` carry the per-project and global drawer
|
|
11
|
+
* styling, which lives on the host rather than in this browser.
|
|
12
|
+
*
|
|
13
|
+
* The fetch callback is handed to the component through the slot registration's
|
|
14
|
+
* `inject` face (the dsh pattern: business callbacks cross from apply-scope to
|
|
15
|
+
* component via the inject factory, never through a global ctx).
|
|
16
|
+
*/
|
|
17
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-runtime' // informational inject edge (loading order)
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots' // SlotMap is reused, not extended
|
|
20
|
+
import {
|
|
21
|
+
GitWorkbenchPanel,
|
|
22
|
+
type GitCommit, type GitOpName, type GitOpPayload, type GitOpResult,
|
|
23
|
+
type WorkbenchStats, type SyncStatus, type WorktreeStatus,
|
|
24
|
+
} from './GitWorkbenchPanel.tsx'
|
|
25
|
+
import type { StyleEntry, StyleScope, StyleSettings } from './themes.ts'
|
|
26
|
+
import { en, zh } from './locales.ts'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This plugin's dictionary namespace. It is outside dsh's `LocaleNamespaceMap`
|
|
30
|
+
* merge table (out-of-tree plugin), so registration uses the single-locale
|
|
31
|
+
* untyped `register` overload provided for that case.
|
|
32
|
+
*/
|
|
33
|
+
const NS = 'gitworkbench'
|
|
34
|
+
|
|
35
|
+
/** Required client services: sessions store + slot registry + connection RPC + locale. */
|
|
36
|
+
export const inject = ['sessions', 'slots', 'connection', 'locale']
|
|
37
|
+
|
|
38
|
+
export function apply(ctx: ClientContext): void {
|
|
39
|
+
// `connection` is a root service; reach it off the root context the apply runs under.
|
|
40
|
+
const connection = (ctx as unknown as { connection: { rpc: { call: (channel: string, endpoint: string, payload: unknown, signal: AbortSignal) => Promise<unknown> } } }).connection
|
|
41
|
+
const locale = (ctx as unknown as {
|
|
42
|
+
locale: { register: (ns: string, id: string, dict: Record<string, string>) => () => void }
|
|
43
|
+
}).locale
|
|
44
|
+
|
|
45
|
+
// One effect per locale: each register call returns its own disposer, and the
|
|
46
|
+
// runtime rejects re-registering a locale a namespace already has (so a reload
|
|
47
|
+
// that failed to dispose fails loudly rather than silently keeping stale copy).
|
|
48
|
+
ctx.effect(() => locale.register(NS, 'zh', zh), 'dsh-ui-gitworkbench: zh dictionary')
|
|
49
|
+
ctx.effect(() => locale.register(NS, 'en', en), 'dsh-ui-gitworkbench: en dictionary')
|
|
50
|
+
|
|
51
|
+
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register(
|
|
52
|
+
{
|
|
53
|
+
name: 'conversation.session.header.actions',
|
|
54
|
+
id: 'git-workbench',
|
|
55
|
+
// After the job-list action (order 20); a low-key stats chip.
|
|
56
|
+
order: 30,
|
|
57
|
+
// Declaring the namespace is what puts the framework's `t` in the
|
|
58
|
+
// component's props — it follows the user's language preference.
|
|
59
|
+
locale: NS,
|
|
60
|
+
inject: () => ({
|
|
61
|
+
// Bound RPC caller the component invokes on mount + poll. Plain args under SRC.
|
|
62
|
+
fetchStats: async (worktreePath: string | undefined, signal: AbortSignal): Promise<WorkbenchStats | null> => {
|
|
63
|
+
const result = await connection.rpc.call(
|
|
64
|
+
'/api',
|
|
65
|
+
'gitWorkbench/stats',
|
|
66
|
+
worktreePath === undefined ? { args: {} } : { args: { worktreePath } },
|
|
67
|
+
signal,
|
|
68
|
+
) as { ok: true; value: WorkbenchStats } | { ok: false; error: { message?: string } }
|
|
69
|
+
return result.ok ? result.value : null
|
|
70
|
+
},
|
|
71
|
+
// On-demand single-file diff. With `commit` it is that commit's change to the
|
|
72
|
+
// file; without it, the working tree (tracked: git diff HEAD --; untracked: synthesized).
|
|
73
|
+
fetchFileDiff: async (worktreePath: string | undefined, path: string, commit: string | undefined, signal: AbortSignal): Promise<string> => {
|
|
74
|
+
const result = await connection.rpc.call(
|
|
75
|
+
'/api',
|
|
76
|
+
'gitWorkbench/fileDiff',
|
|
77
|
+
{ args: { worktreePath: worktreePath ?? '', path, ...commit === undefined ? {} : { commit } } },
|
|
78
|
+
signal,
|
|
79
|
+
) as { ok: true; value: { diff: string } } | { ok: false; error: { message?: string } }
|
|
80
|
+
return result.ok ? result.value.diff : ''
|
|
81
|
+
},
|
|
82
|
+
// One commit's change set, in the same shape as `stats` — the drawer's tree and
|
|
83
|
+
// diff panes render it through the same code path as the working tree.
|
|
84
|
+
fetchCommitStats: async (worktreePath: string | undefined, hash: string, signal: AbortSignal): Promise<WorkbenchStats | null> => {
|
|
85
|
+
const result = await connection.rpc.call(
|
|
86
|
+
'/api',
|
|
87
|
+
'gitWorkbench/commitStats',
|
|
88
|
+
{ args: { worktreePath: worktreePath ?? '', hash } },
|
|
89
|
+
signal,
|
|
90
|
+
) as { ok: true; value: WorkbenchStats } | { ok: false; error: { message?: string } }
|
|
91
|
+
return result.ok ? result.value : null
|
|
92
|
+
},
|
|
93
|
+
// One page of the commit log past what `stats` bundles, so the history
|
|
94
|
+
// list can grow instead of stopping at the first page.
|
|
95
|
+
fetchCommits: async (worktreePath: string | undefined, ref: string, skip: number, limit: number, signal: AbortSignal): Promise<{ commits: GitCommit[]; hasMore: boolean } | null> => {
|
|
96
|
+
const result = await connection.rpc.call(
|
|
97
|
+
'/api',
|
|
98
|
+
'gitWorkbench/commits',
|
|
99
|
+
{ args: { worktreePath: worktreePath ?? '', ref, skip, limit } },
|
|
100
|
+
signal,
|
|
101
|
+
) as { ok: true; value: { commits: GitCommit[]; hasMore: boolean } } | { ok: false; error: { message?: string } }
|
|
102
|
+
return result.ok ? result.value : null
|
|
103
|
+
},
|
|
104
|
+
// Two refs compared as `base...head`, in the same shape as every other
|
|
105
|
+
// view, so the drawer's tree and diff panes render it unchanged.
|
|
106
|
+
fetchCompare: async (worktreePath: string | undefined, base: string, head: string, signal: AbortSignal): Promise<WorkbenchStats | null> => {
|
|
107
|
+
const result = await connection.rpc.call(
|
|
108
|
+
'/api',
|
|
109
|
+
'gitWorkbench/compareRefs',
|
|
110
|
+
{ args: { worktreePath: worktreePath ?? '', base, head } },
|
|
111
|
+
signal,
|
|
112
|
+
) as { ok: true; value: WorkbenchStats } | { ok: false; error: { message?: string } }
|
|
113
|
+
return result.ok ? result.value : null
|
|
114
|
+
},
|
|
115
|
+
// The session's binding plus EVERY worktree of the surrounding repository.
|
|
116
|
+
// Polled in step with the stats, so the chip tracks the agent's enter/exit
|
|
117
|
+
// calls and the drawer's source list tracks worktrees added outside dsh.
|
|
118
|
+
fetchWorktreeStatus: async (sessionId: string, repoPath: string | undefined, signal: AbortSignal): Promise<WorktreeStatus | null> => {
|
|
119
|
+
const result = await connection.rpc.call(
|
|
120
|
+
'/api',
|
|
121
|
+
'gitWorkbench/worktreeStatus',
|
|
122
|
+
{ args: { sessionId, repoPath: repoPath ?? '' } },
|
|
123
|
+
signal,
|
|
124
|
+
) as { ok: true; value: WorktreeStatus } | { ok: false; error: { message?: string } }
|
|
125
|
+
return result.ok ? result.value : null
|
|
126
|
+
},
|
|
127
|
+
// The session's binding and nothing else. This one reads the bindings
|
|
128
|
+
// JSON and spawns no git, which is what lets the chip keep watching for
|
|
129
|
+
// an agent's `worktree_enter` with the drawer shut — where the full
|
|
130
|
+
// status call (rev-parse + worktree list + branch list) would be a git
|
|
131
|
+
// spawn per session header per tick. A disagreement here is what
|
|
132
|
+
// triggers one `worktreeStatus` to repaint everything.
|
|
133
|
+
fetchSessionBinding: async (sessionId: string, signal: AbortSignal): Promise<{ worktreePath: string | null; name: string | null } | null> => {
|
|
134
|
+
const result = await connection.rpc.call(
|
|
135
|
+
'/api',
|
|
136
|
+
'gitWorkbench/sessionWorktree',
|
|
137
|
+
{ args: { sessionId } },
|
|
138
|
+
signal,
|
|
139
|
+
) as { ok: true; value: { worktreePath: string | null; name: string | null } } | { ok: false; error: { message?: string } }
|
|
140
|
+
return result.ok ? result.value : null
|
|
141
|
+
},
|
|
142
|
+
// Drawer styling for this directory: the project's scope and the global
|
|
143
|
+
// one, unresolved — the menu edits each separately, so it needs both.
|
|
144
|
+
fetchStyle: async (worktreePath: string | undefined, signal: AbortSignal): Promise<StyleSettings | null> => {
|
|
145
|
+
const result = await connection.rpc.call(
|
|
146
|
+
'/api',
|
|
147
|
+
'gitWorkbench/styleGet',
|
|
148
|
+
{ args: { worktreePath: worktreePath ?? '' } },
|
|
149
|
+
signal,
|
|
150
|
+
) as { ok: true; value: StyleSettings } | { ok: false; error: { message?: string } }
|
|
151
|
+
return result.ok ? result.value : null
|
|
152
|
+
},
|
|
153
|
+
// Replace one scope's styling. The host validates and clamps everything
|
|
154
|
+
// in the entry, so a rejected value comes back as ok:false with a reason.
|
|
155
|
+
saveStyle: async (worktreePath: string | undefined, scope: StyleScope, entry: StyleEntry, signal: AbortSignal): Promise<{ ok: boolean; error?: string }> => {
|
|
156
|
+
const result = await connection.rpc.call(
|
|
157
|
+
'/api',
|
|
158
|
+
'gitWorkbench/styleSet',
|
|
159
|
+
{ args: { worktreePath: worktreePath ?? '', scope, entry } },
|
|
160
|
+
signal,
|
|
161
|
+
// Flat rather than a discriminated union: this package compiles with
|
|
162
|
+
// `strictNullChecks` off, which cannot narrow one, and this is the only
|
|
163
|
+
// caller that reads the transport's own error rather than ignoring it.
|
|
164
|
+
) as { ok: boolean; value?: { ok: boolean; error?: string }; error?: { message?: string } }
|
|
165
|
+
if (result.ok && result.value !== undefined) return result.value
|
|
166
|
+
return { ok: false, error: result.error?.message ?? 'rpc failed' }
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// ---- write operations ----
|
|
170
|
+
//
|
|
171
|
+
// Each returns the host's own GitOpResult. A transport failure is folded
|
|
172
|
+
// into the same shape rather than thrown: the drawer shows one banner for
|
|
173
|
+
// "the operation failed", and it should not matter to that banner whether
|
|
174
|
+
// git refused or the socket did.
|
|
175
|
+
fetchSync: async (worktreePath: string | undefined, signal: AbortSignal): Promise<SyncStatus | null> => {
|
|
176
|
+
const result = await connection.rpc.call(
|
|
177
|
+
'/api',
|
|
178
|
+
'gitWorkbench/syncStatus',
|
|
179
|
+
{ args: { worktreePath: worktreePath ?? '' } },
|
|
180
|
+
signal,
|
|
181
|
+
) as { ok: true; value: SyncStatus } | { ok: false; error: { message?: string } }
|
|
182
|
+
return result.ok ? result.value : null
|
|
183
|
+
},
|
|
184
|
+
runGitOp: async (op: GitOpName, worktreePath: string | undefined, payload: GitOpPayload, signal: AbortSignal): Promise<GitOpResult> => {
|
|
185
|
+
const result = await connection.rpc.call(
|
|
186
|
+
'/api',
|
|
187
|
+
`gitWorkbench/${op}`,
|
|
188
|
+
{ args: { worktreePath: worktreePath ?? '', ...payload } },
|
|
189
|
+
signal,
|
|
190
|
+
) as { ok: boolean; value?: GitOpResult; error?: { message?: string } }
|
|
191
|
+
if (result.ok && result.value !== undefined) return result.value
|
|
192
|
+
return { ok: false, failure: 'unknown', error: result.error?.message ?? 'rpc failed' }
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
GitWorkbenchPanel,
|
|
197
|
+
))
|
|
198
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictionaries for this plugin's UI copy, registered into the app's locale
|
|
3
|
+
* runtime under the `gitworkbench` namespace so the panel follows the user's
|
|
4
|
+
* language preference instead of shipping one hardcoded language.
|
|
5
|
+
*
|
|
6
|
+
* The namespace lives outside dsh's `LocaleNamespaceMap` merge table (this is an
|
|
7
|
+
* out-of-tree plugin), so registration uses `ctx.locale.register(ns, locale, dict)`
|
|
8
|
+
* — the single-locale untyped form the runtime provides for exactly that case.
|
|
9
|
+
* Keys ARE compile-time checked — both dictionaries are `Record<WorkbenchKey, string>`,
|
|
10
|
+
* so tsc keeps them in step — but a lookup miss still renders the key itself
|
|
11
|
+
* rather than blank.
|
|
12
|
+
*
|
|
13
|
+
* Values are templates with `{name}` placeholders.
|
|
14
|
+
*
|
|
15
|
+
* English wording follows git's own vocabulary rather than translating the
|
|
16
|
+
* Chinese literally: a change set is `added / modified / deleted`, matching
|
|
17
|
+
* `git status` and the file badges (A / M / D) the tree already renders.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Every key this plugin looks up — the two dictionaries below must both cover it. */
|
|
21
|
+
export type WorkbenchKey =
|
|
22
|
+
| 'aheadTitle' | 'behindTitle' | 'files'
|
|
23
|
+
| 'drawerLabel' | 'totalsDim' | 'refresh' | 'close'
|
|
24
|
+
| 'tabsLabel' | 'tabChanges' | 'tabHistory' | 'tabCompare'
|
|
25
|
+
| 'sourceLabel' | 'workingTree'
|
|
26
|
+
| 'loadingCommit' | 'renamedFrom' | 'binaryFile' | 'loadingDiff' | 'noTextDiff'
|
|
27
|
+
| 'noCommits' | 'historyLabel' | 'historyEnd' | 'loading' | 'maximize' | 'restore'
|
|
28
|
+
| 'compareBase' | 'compareHead' | 'comparePick' | 'compareCommits' | 'loadingCompare' | 'noBranches'
|
|
29
|
+
| 'refSearch' | 'refNone' | 'refCount' | 'refTruncated' | 'refWorktrees' | 'refBranches' | 'historyRefLabel'
|
|
30
|
+
| 'settings' | 'themeMode' | 'themePalette' | 'themeScope' | 'themeBackground' | 'themeCss'
|
|
31
|
+
| 'modeSystem' | 'modeLight' | 'modeDark'
|
|
32
|
+
| 'scopeProject' | 'scopeGlobal' | 'scopeGlobalHint' | 'scopeNoRepo'
|
|
33
|
+
| 'bgNone' | 'bgChoose' | 'bgClear' | 'bgBlur' | 'bgVeil' | 'bgWorking' | 'bgFailed' | 'bgTooBig'
|
|
34
|
+
| 'cssPlaceholder' | 'cssImport' | 'cssApply' | 'cssUnapplied' | 'styleFailed'
|
|
35
|
+
| 'resizeLabel' | 'resizeCommits' | 'resizeTree'
|
|
36
|
+
| 'expandAll' | 'collapseAll' | 'noBranch'
|
|
37
|
+
| 'copyCommit' | 'copiedCommit'
|
|
38
|
+
// write operations
|
|
39
|
+
| 'syncLabel' | 'noUpstream' | 'noUpstreamHint' | 'upToDate' | 'opRunning'
|
|
40
|
+
| 'fetch' | 'pull' | 'push' | 'publish' | 'pushSetUpstream'
|
|
41
|
+
| 'pullModeLabel' | 'pullFf' | 'pullRebase' | 'pullMerge'
|
|
42
|
+
| 'stage' | 'unstage' | 'stageAll' | 'unstageAll' | 'stagedCount'
|
|
43
|
+
| 'commit' | 'amend' | 'commitPlaceholder' | 'commitNeedMessage' | 'commitLead'
|
|
44
|
+
| 'op.ok.stage' | 'op.ok.unstage' | 'op.ok.commit' | 'op.ok.fetch' | 'op.ok.pull' | 'op.ok.push'
|
|
45
|
+
| 'op.fail.auth' | 'op.fail.network' | 'op.fail.no-upstream' | 'op.fail.diverged' | 'op.fail.conflict'
|
|
46
|
+
| 'op.fail.nothing-to-commit' | 'op.fail.dirty' | 'op.fail.unknown'
|
|
47
|
+
|
|
48
|
+
export const zh: Record<WorkbenchKey, string> = {
|
|
49
|
+
aheadTitle: '领先上游 {count} 个提交',
|
|
50
|
+
behindTitle: '落后上游 {count} 个提交',
|
|
51
|
+
files: '{count} 文件',
|
|
52
|
+
drawerLabel: 'git 变更',
|
|
53
|
+
totalsDim: '{added} 新 · {modified} 改 · {deleted} 删',
|
|
54
|
+
refresh: '刷新',
|
|
55
|
+
close: '关闭',
|
|
56
|
+
tabsLabel: '变更视图',
|
|
57
|
+
tabChanges: '变更',
|
|
58
|
+
tabHistory: '历史',
|
|
59
|
+
tabCompare: '对比',
|
|
60
|
+
historyEnd: '已到最早的提交',
|
|
61
|
+
loading: '加载中…',
|
|
62
|
+
maximize: '最大化',
|
|
63
|
+
restore: '还原',
|
|
64
|
+
compareBase: '基准',
|
|
65
|
+
compareHead: '对比',
|
|
66
|
+
comparePick: '选择两个不同的分支进行对比',
|
|
67
|
+
compareCommits: '{count} 个提交(自共同祖先起)',
|
|
68
|
+
loadingCompare: '加载对比…',
|
|
69
|
+
noBranches: '没有可对比的分支',
|
|
70
|
+
refSearch: '筛选分支…',
|
|
71
|
+
refNone: '没有匹配的分支',
|
|
72
|
+
refCount: '{shown} / {total}',
|
|
73
|
+
refTruncated: '仅显示最近的若干分支',
|
|
74
|
+
refWorktrees: '有工作树的分支',
|
|
75
|
+
refBranches: '其他分支',
|
|
76
|
+
historyRefLabel: '分支',
|
|
77
|
+
settings: '设置',
|
|
78
|
+
themeMode: '明暗',
|
|
79
|
+
themePalette: '配色',
|
|
80
|
+
themeScope: '作用范围',
|
|
81
|
+
themeBackground: '背景图',
|
|
82
|
+
themeCss: '自定义 CSS',
|
|
83
|
+
modeSystem: '跟随',
|
|
84
|
+
modeLight: '亮色',
|
|
85
|
+
modeDark: '暗色',
|
|
86
|
+
scopeProject: '本项目',
|
|
87
|
+
scopeGlobal: '全局',
|
|
88
|
+
scopeGlobalHint: '所有项目;本项目单独设置时以本项目为准',
|
|
89
|
+
scopeNoRepo: '当前目录不是 git 仓库,只能设置全局',
|
|
90
|
+
bgNone: '未设置背景图',
|
|
91
|
+
bgChoose: '选择图片…',
|
|
92
|
+
bgClear: '清除',
|
|
93
|
+
bgBlur: '虚化',
|
|
94
|
+
bgVeil: '遮罩',
|
|
95
|
+
bgWorking: '处理中…',
|
|
96
|
+
bgFailed: '这个文件无法作为图片读取',
|
|
97
|
+
bgTooBig: '图片过大,请换一张',
|
|
98
|
+
cssPlaceholder: '例如 [data-gs-part="card"] { --gs-accent: #ff0066; }',
|
|
99
|
+
cssImport: '导入…',
|
|
100
|
+
cssApply: '应用',
|
|
101
|
+
cssUnapplied: '有未应用的修改',
|
|
102
|
+
styleFailed: '保存失败',
|
|
103
|
+
resizeLabel: '拖动调整抽屉宽度',
|
|
104
|
+
resizeCommits: '拖动调整提交列表宽度',
|
|
105
|
+
resizeTree: '拖动调整文件树宽度',
|
|
106
|
+
sourceLabel: '统计来源切换',
|
|
107
|
+
workingTree: '工作区',
|
|
108
|
+
loadingCommit: '加载提交…',
|
|
109
|
+
renamedFrom: '重命名自',
|
|
110
|
+
binaryFile: '二进制文件 —— 不显示文本 diff',
|
|
111
|
+
loadingDiff: '加载 diff…',
|
|
112
|
+
noTextDiff: '无文本差异',
|
|
113
|
+
noCommits: '无提交历史',
|
|
114
|
+
historyLabel: '提交历史',
|
|
115
|
+
expandAll: '展开全部',
|
|
116
|
+
collapseAll: '收起全部',
|
|
117
|
+
noBranch: '(无分支)',
|
|
118
|
+
copyCommit: '复制提交说明',
|
|
119
|
+
copiedCommit: '已复制',
|
|
120
|
+
syncLabel: '与远端同步',
|
|
121
|
+
noUpstream: '未跟踪远端分支',
|
|
122
|
+
noUpstreamHint: '当前分支没有上游分支,先推送一次即可建立',
|
|
123
|
+
upToDate: '已同步',
|
|
124
|
+
opRunning: '执行中…',
|
|
125
|
+
fetch: '拉取远端信息',
|
|
126
|
+
pull: '拉取',
|
|
127
|
+
push: '推送',
|
|
128
|
+
publish: '发布分支',
|
|
129
|
+
pushSetUpstream: '首次推送,将建立 origin 上的同名分支',
|
|
130
|
+
pullModeLabel: '拉取方式',
|
|
131
|
+
pullFf: '仅快进',
|
|
132
|
+
pullRebase: '变基',
|
|
133
|
+
pullMerge: '合并',
|
|
134
|
+
stage: '暂存',
|
|
135
|
+
unstage: '取消暂存',
|
|
136
|
+
stageAll: '勾选全部',
|
|
137
|
+
unstageAll: '取消全部勾选',
|
|
138
|
+
stagedCount: '已勾选 {count} 个',
|
|
139
|
+
commit: '提交',
|
|
140
|
+
amend: '修改上一条提交',
|
|
141
|
+
commitPlaceholder: '提交说明(Ctrl+Enter 提交)',
|
|
142
|
+
commitNeedMessage: '请先填写提交说明',
|
|
143
|
+
commitLead: '先勾选文件暂存,再提交',
|
|
144
|
+
'op.ok.stage': '已暂存',
|
|
145
|
+
'op.ok.unstage': '已取消暂存',
|
|
146
|
+
'op.ok.commit': '提交成功',
|
|
147
|
+
'op.ok.fetch': '已获取远端信息',
|
|
148
|
+
'op.ok.pull': '拉取完成',
|
|
149
|
+
'op.ok.push': '推送成功',
|
|
150
|
+
'op.fail.auth': '认证失败。凭据提示已被禁用,请先在终端里配置好凭据再重试。',
|
|
151
|
+
'op.fail.network': '网络不可达:主机名解析失败或连接不上。检查网络与远程地址后重试。',
|
|
152
|
+
'op.fail.no-upstream': '当前分支没有上游分支。',
|
|
153
|
+
'op.fail.diverged': '远端有本地没有的提交。先拉取再推送——这里不会强制覆盖。',
|
|
154
|
+
'op.fail.conflict': '出现冲突,工作区已被改动。请在编辑器里解决冲突后继续。',
|
|
155
|
+
'op.fail.nothing-to-commit': '暂存区是空的,没有可提交的内容。',
|
|
156
|
+
'op.fail.dirty': '本地改动会被覆盖。先提交或暂存它们。',
|
|
157
|
+
'op.fail.unknown': '操作失败。',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const en: Record<WorkbenchKey, string> = {
|
|
161
|
+
aheadTitle: '{count} commits ahead of upstream',
|
|
162
|
+
behindTitle: '{count} commits behind upstream',
|
|
163
|
+
files: '{count} files',
|
|
164
|
+
drawerLabel: 'git changes',
|
|
165
|
+
totalsDim: '{added} added · {modified} modified · {deleted} deleted',
|
|
166
|
+
refresh: 'Refresh',
|
|
167
|
+
close: 'Close',
|
|
168
|
+
tabsLabel: 'Change views',
|
|
169
|
+
tabChanges: 'Changes',
|
|
170
|
+
tabHistory: 'History',
|
|
171
|
+
tabCompare: 'Compare',
|
|
172
|
+
historyEnd: 'Start of history',
|
|
173
|
+
loading: 'Loading…',
|
|
174
|
+
maximize: 'Maximize',
|
|
175
|
+
restore: 'Restore',
|
|
176
|
+
compareBase: 'Base',
|
|
177
|
+
compareHead: 'Compare',
|
|
178
|
+
comparePick: 'Pick two different branches to compare',
|
|
179
|
+
compareCommits: '{count} commits since they diverged',
|
|
180
|
+
loadingCompare: 'Loading comparison…',
|
|
181
|
+
noBranches: 'No branches to compare',
|
|
182
|
+
refSearch: 'Filter branches…',
|
|
183
|
+
refNone: 'No matching branch',
|
|
184
|
+
refCount: '{shown} / {total}',
|
|
185
|
+
refTruncated: 'showing the most recent only',
|
|
186
|
+
refWorktrees: 'With a worktree',
|
|
187
|
+
refBranches: 'Other branches',
|
|
188
|
+
historyRefLabel: 'Branch',
|
|
189
|
+
settings: 'Settings',
|
|
190
|
+
themeMode: 'Mode',
|
|
191
|
+
themePalette: 'Palette',
|
|
192
|
+
themeScope: 'Applies to',
|
|
193
|
+
themeBackground: 'Background',
|
|
194
|
+
themeCss: 'Custom CSS',
|
|
195
|
+
modeSystem: 'Match app',
|
|
196
|
+
modeLight: 'Light',
|
|
197
|
+
modeDark: 'Dark',
|
|
198
|
+
scopeProject: 'This project',
|
|
199
|
+
scopeGlobal: 'Global',
|
|
200
|
+
scopeGlobalHint: 'Every project; a project setting wins over this one',
|
|
201
|
+
scopeNoRepo: 'Not a git repository — global only',
|
|
202
|
+
bgNone: 'No background image',
|
|
203
|
+
bgChoose: 'Choose image…',
|
|
204
|
+
bgClear: 'Clear',
|
|
205
|
+
bgBlur: 'Blur',
|
|
206
|
+
bgVeil: 'Veil',
|
|
207
|
+
bgWorking: 'Working…',
|
|
208
|
+
bgFailed: 'That file could not be read as an image',
|
|
209
|
+
bgTooBig: 'Image too large — pick another',
|
|
210
|
+
cssPlaceholder: 'e.g. [data-gs-part="card"] { --gs-accent: #ff0066; }',
|
|
211
|
+
cssImport: 'Import…',
|
|
212
|
+
cssApply: 'Apply',
|
|
213
|
+
cssUnapplied: 'Unapplied changes',
|
|
214
|
+
styleFailed: 'Could not save',
|
|
215
|
+
resizeLabel: 'Drag to resize the drawer',
|
|
216
|
+
resizeCommits: 'Drag to resize the commit list',
|
|
217
|
+
resizeTree: 'Drag to resize the file tree',
|
|
218
|
+
sourceLabel: 'Switch stats source',
|
|
219
|
+
workingTree: 'Working tree',
|
|
220
|
+
loadingCommit: 'Loading commit…',
|
|
221
|
+
renamedFrom: 'Renamed from',
|
|
222
|
+
binaryFile: 'Binary file — no text diff',
|
|
223
|
+
loadingDiff: 'Loading diff…',
|
|
224
|
+
noTextDiff: 'No text changes',
|
|
225
|
+
noCommits: 'No commit history',
|
|
226
|
+
historyLabel: 'Commit history',
|
|
227
|
+
expandAll: 'Expand all',
|
|
228
|
+
collapseAll: 'Collapse all',
|
|
229
|
+
noBranch: '(no branch)',
|
|
230
|
+
copyCommit: 'Copy message',
|
|
231
|
+
copiedCommit: 'Copied',
|
|
232
|
+
syncLabel: 'Sync with remote',
|
|
233
|
+
noUpstream: 'No upstream branch',
|
|
234
|
+
noUpstreamHint: 'This branch tracks nothing yet; one push establishes it',
|
|
235
|
+
upToDate: 'Up to date',
|
|
236
|
+
opRunning: 'Working…',
|
|
237
|
+
fetch: 'Fetch',
|
|
238
|
+
pull: 'Pull',
|
|
239
|
+
push: 'Push',
|
|
240
|
+
publish: 'Publish branch',
|
|
241
|
+
pushSetUpstream: 'First push — creates the branch on origin',
|
|
242
|
+
pullModeLabel: 'Pull strategy',
|
|
243
|
+
pullFf: 'Fast-forward only',
|
|
244
|
+
pullRebase: 'Rebase',
|
|
245
|
+
pullMerge: 'Merge',
|
|
246
|
+
stage: 'Stage',
|
|
247
|
+
unstage: 'Unstage',
|
|
248
|
+
stageAll: 'Tick all',
|
|
249
|
+
unstageAll: 'Untick all',
|
|
250
|
+
stagedCount: '{count} ticked',
|
|
251
|
+
commit: 'Commit',
|
|
252
|
+
amend: 'Amend last commit',
|
|
253
|
+
commitPlaceholder: 'Commit message (Ctrl+Enter to commit)',
|
|
254
|
+
commitNeedMessage: 'Write a commit message first',
|
|
255
|
+
commitLead: 'Tick to stage, then commit',
|
|
256
|
+
'op.ok.stage': 'Staged',
|
|
257
|
+
'op.ok.unstage': 'Unstaged',
|
|
258
|
+
'op.ok.commit': 'Committed',
|
|
259
|
+
'op.ok.fetch': 'Fetched',
|
|
260
|
+
'op.ok.pull': 'Pulled',
|
|
261
|
+
'op.ok.push': 'Pushed',
|
|
262
|
+
'op.fail.auth': 'Authentication failed. Credential prompts are disabled here — set your credentials up in a terminal first.',
|
|
263
|
+
'op.fail.network': 'The network was unreachable — the host could not be resolved or the connection failed. Check connectivity and the remote URL, then retry.',
|
|
264
|
+
'op.fail.no-upstream': 'This branch has no upstream.',
|
|
265
|
+
'op.fail.diverged': 'The remote has commits this branch does not. Pull first — nothing here force-pushes.',
|
|
266
|
+
'op.fail.conflict': 'Conflicts. The working tree has been changed; resolve them in the editor before continuing.',
|
|
267
|
+
'op.fail.nothing-to-commit': 'Nothing staged to commit.',
|
|
268
|
+
'op.fail.dirty': 'Local changes would be overwritten. Commit or stash them first.',
|
|
269
|
+
'op.fail.unknown': 'The operation failed.',
|
|
270
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing for the drawer's "an operation is running" appearance.
|
|
3
|
+
*
|
|
4
|
+
* `busy` in GitWorkbenchPanel names two different facts. The first is a guard: an
|
|
5
|
+
* operation is in flight, so every control refuses to start another. That has
|
|
6
|
+
* to be true on the same tick as the click, or a double click fires two git
|
|
7
|
+
* calls. The second is an announcement — the drawer dims what it has disabled,
|
|
8
|
+
* so the user can see why a click did nothing.
|
|
9
|
+
*
|
|
10
|
+
* Only the announcement is worth pacing. A tick IS a git call (`add` on the way
|
|
11
|
+
* in, `restore --staged` on the way out) and it completes in about 150ms; that
|
|
12
|
+
* was long enough to fade the whole sync row to `opacity: .45` and back on
|
|
13
|
+
* every single click, which reads as a flicker rather than as feedback. Nobody
|
|
14
|
+
* can act on a message that brief — its only effect was the blink.
|
|
15
|
+
*
|
|
16
|
+
* So the appearance waits, and once it arrives it stays. A threshold on its own
|
|
17
|
+
* would just move the boundary: an operation finishing a little past it would
|
|
18
|
+
* paint the dim for a few milliseconds, which is the same blink one notch
|
|
19
|
+
* slower. The hold is what makes the dim always mean something.
|
|
20
|
+
*
|
|
21
|
+
* These live outside the panel so they can be tested — importing
|
|
22
|
+
* GitWorkbenchPanel.tsx pulls a CSS module and React, which a node test environment
|
|
23
|
+
* cannot load.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How long an operation must run before the drawer dims what it disabled.
|
|
28
|
+
*
|
|
29
|
+
* Above the ~150ms a stage/unstage round trip measured on the live drawer, so
|
|
30
|
+
* the operation behind every tick never paints at all.
|
|
31
|
+
*/
|
|
32
|
+
export const BUSY_DELAY_MS = 220
|
|
33
|
+
|
|
34
|
+
/** Once the dim is up, the shortest time it stays — long enough to read. */
|
|
35
|
+
export const BUSY_HOLD_MS = 320
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a control is disabled ONLY by an operation too young to report.
|
|
39
|
+
*
|
|
40
|
+
* Such a control keeps its full strength: it is already refusing clicks, and
|
|
41
|
+
* saying so costs more than it gives when the answer arrives in 150ms.
|
|
42
|
+
*
|
|
43
|
+
* @param running - whether any git operation is in flight.
|
|
44
|
+
* @param sustained - whether that operation has lasted long enough to report.
|
|
45
|
+
* @param steady - whether this control is disabled for a reason of its own,
|
|
46
|
+
* like Pull with no upstream. Those keep their dim: it states an
|
|
47
|
+
* unavailable action, and dropping it mid-operation would be a new flicker.
|
|
48
|
+
*/
|
|
49
|
+
export function quietlyDisabled(running: boolean, sustained: boolean, steady: boolean): boolean {
|
|
50
|
+
return running && !sustained && !steady
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Milliseconds the dim must stay up before it may be taken down.
|
|
55
|
+
*
|
|
56
|
+
* @param shownAt - when the dim appeared, in `Date.now()` terms.
|
|
57
|
+
* @param now - the current time.
|
|
58
|
+
* @param hold - the minimum visible duration.
|
|
59
|
+
* @returns the remaining time, never negative — a negative delay would
|
|
60
|
+
* schedule a timer into the past and make the dim vanish on the same frame
|
|
61
|
+
* it was granted.
|
|
62
|
+
*/
|
|
63
|
+
export function holdRemaining(shownAt: number, now: number, hold: number = BUSY_HOLD_MS): number {
|
|
64
|
+
return Math.max(0, hold - (now - shownAt))
|
|
65
|
+
}
|