dsh-remote-vs 0.9.3 → 0.11.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/README.en.md +13 -10
- package/README.md +14 -9
- package/cordis.patch.yml +10 -5
- package/lib/client.js +3319 -1093
- package/lib/index.js +211 -9
- package/lib/types/client/api.d.ts +12 -0
- package/lib/types/client/composer.d.ts +5 -1
- package/lib/types/client/exec-output.d.ts +58 -0
- package/lib/types/client/i18n.d.ts +1 -1
- package/lib/types/client/scm/CommitPane.d.ts +73 -0
- package/lib/types/client/scm/ExecPanel.d.ts +34 -0
- package/lib/types/client/scm/HistoryPanel.d.ts +12 -4
- package/lib/types/client/scm/actions.d.ts +50 -3
- package/lib/types/client/scm/commit-model.d.ts +119 -0
- package/lib/types/client/store.d.ts +9 -3
- package/lib/types/client/vsc/kit.d.ts +16 -0
- package/lib/types/git-format.d.ts +5 -0
- package/lib/types/git-ops.d.ts +57 -2
- package/lib/types/index.d.ts +5 -0
- package/lib/types/pool.d.ts +9 -0
- package/lib/types/protocol.d.ts +23 -0
- package/lib/types/timeouts.d.ts +17 -2
- package/package.json +37 -37
- package/src/client/SettingsHosts.tsx +2 -1
- package/src/client/api.ts +61 -0
- package/src/client/composer.ts +11 -2
- package/src/client/exec-output.ts +129 -0
- package/src/client/i18n.ts +133 -4
- package/src/client/scm/CommitPane.tsx +726 -0
- package/src/client/scm/ExecPanel.tsx +124 -0
- package/src/client/scm/HistoryPanel.tsx +483 -104
- package/src/client/scm/ScmPanel.tsx +247 -9
- package/src/client/scm/actions.ts +93 -10
- package/src/client/scm/commit-model.ts +317 -0
- package/src/client/store.tsx +13 -6
- package/src/client/vsc/kit.tsx +71 -0
- package/src/client/vsc/vsc.css +647 -0
- package/src/git-format.ts +7 -3
- package/src/git-ops.ts +83 -7
- package/src/index.ts +10 -2
- package/src/pool.ts +84 -4
- package/src/protocol.ts +14 -0
- package/src/routes.ts +135 -1
- package/src/timeouts.ts +18 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model behind the history panel's commit pane.
|
|
3
|
+
*
|
|
4
|
+
* The pane answers three questions about the selected commit — who/when/what
|
|
5
|
+
* it says (`details`), which files it touched (`content`) and what changed
|
|
6
|
+
* (`diff`). Only the last one needs the patch, so this module turns one
|
|
7
|
+
* `git show --patch` text into the per-file blocks the view renders: file
|
|
8
|
+
* header (path, rename source, status letter, +/− counts), hunk rows carrying
|
|
9
|
+
* BOTH line numbers (a patch already narrates the pairing), and `skip` rows
|
|
10
|
+
* where a hunk jumps ahead.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure so the local self-check (the `scripts/` and `.spike/`
|
|
13
|
+
* harnesses are gitignored and not shipped) can pin it, including a replay
|
|
14
|
+
* against a real repository's `git show --stat`: a hand-written expectation
|
|
15
|
+
* would only encode my idea of git's output.
|
|
16
|
+
*
|
|
17
|
+
* No full-file context is available: a commit patch is read-only and git only
|
|
18
|
+
* ships the hunks. That is deliberate — building the two full revisions per
|
|
19
|
+
* file would cost one `git show <rev>:<path>` per side per file just to paint
|
|
20
|
+
* unchanged lines nobody asked for.
|
|
21
|
+
*/
|
|
22
|
+
import { parseUnifiedDiff } from '../../git-ops.ts'
|
|
23
|
+
import type { GitCommitFile } from '../../protocol.ts'
|
|
24
|
+
|
|
25
|
+
/** The pane's three views, in strip order. */
|
|
26
|
+
export const HISTORY_TABS = ['details', 'content', 'diff'] as const
|
|
27
|
+
|
|
28
|
+
export type HistoryTab = (typeof HISTORY_TABS)[number]
|
|
29
|
+
|
|
30
|
+
/** Guard for persisted values (`localStorage` may hold anything). */
|
|
31
|
+
export function isHistoryTab(value: unknown): value is HistoryTab {
|
|
32
|
+
return typeof value === 'string' && (HISTORY_TABS as readonly string[]).includes(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Keyboard navigation for the pane's tab strip: arrows cycle (wrapping, so the
|
|
37
|
+
* last tab's → returns to the first), Home/End jump to the ends. `undefined`
|
|
38
|
+
* means "not a navigation key" and the caller leaves the event alone.
|
|
39
|
+
*/
|
|
40
|
+
export function stepHistoryTab(current: HistoryTab, key: string): HistoryTab | undefined {
|
|
41
|
+
const index = HISTORY_TABS.indexOf(current)
|
|
42
|
+
if (key === 'Home') return HISTORY_TABS[0]
|
|
43
|
+
if (key === 'End') return HISTORY_TABS[HISTORY_TABS.length - 1]
|
|
44
|
+
if (key === 'ArrowLeft') return HISTORY_TABS[(index - 1 + HISTORY_TABS.length) % HISTORY_TABS.length]
|
|
45
|
+
if (key === 'ArrowRight') return HISTORY_TABS[(index + 1) % HISTORY_TABS.length]
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The state the 差异 tab's lazy read keeps for one commit. */
|
|
50
|
+
export interface PatchLoadState {
|
|
51
|
+
key: string
|
|
52
|
+
text?: string
|
|
53
|
+
truncated?: boolean
|
|
54
|
+
error?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the 差异 tab still has to read the patch for `key`.
|
|
59
|
+
*
|
|
60
|
+
* The subtle case is a SUPERSEDED read: switching the tab or the commit while
|
|
61
|
+
* a read is in flight used to leave the key marker behind with no data, and an
|
|
62
|
+
* equality-only guard (`state.key === key`) then treated that marker as
|
|
63
|
+
* "already loaded" forever — the reported "点击文件后差异一直说没有改动" bug.
|
|
64
|
+
* A marker without text/error therefore means RETRY, never "done".
|
|
65
|
+
*/
|
|
66
|
+
export function needsPatchLoad(state: PatchLoadState, key: string, tab: HistoryTab): boolean {
|
|
67
|
+
if (tab !== 'diff' || key === '') return false
|
|
68
|
+
if (state.key !== key) return true
|
|
69
|
+
return state.text === undefined && state.error === undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One rendered patch line. */
|
|
73
|
+
export interface PatchRow {
|
|
74
|
+
kind: 'context' | 'add' | 'del' | 'skip'
|
|
75
|
+
/** Old-revision line number; null on added lines and skip markers. */
|
|
76
|
+
oldLine: number | null
|
|
77
|
+
/** New-revision line number; null on deleted lines and skip markers. */
|
|
78
|
+
newLine: number | null
|
|
79
|
+
/** Line content without the leading ` ` / `+` / `-` marker. */
|
|
80
|
+
text: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One file block of a commit patch. */
|
|
84
|
+
export interface CommitPatchFile {
|
|
85
|
+
/** Destination path (the source for deletions). */
|
|
86
|
+
path: string
|
|
87
|
+
/** Source path when the block is a rename/copy (equals `path` otherwise). */
|
|
88
|
+
oldPath: string
|
|
89
|
+
status: string
|
|
90
|
+
binary: boolean
|
|
91
|
+
added: number
|
|
92
|
+
deleted: number
|
|
93
|
+
rows: PatchRow[]
|
|
94
|
+
/** True when the global row cap kept this block's rows (partly) out. */
|
|
95
|
+
truncated: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CommitPatchModel {
|
|
99
|
+
files: CommitPatchFile[]
|
|
100
|
+
added: number
|
|
101
|
+
deleted: number
|
|
102
|
+
/** True when the row cap cut the model (later blocks may have no rows). */
|
|
103
|
+
truncated: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Rendered-row ceiling for one commit patch.
|
|
108
|
+
*
|
|
109
|
+
* A commit that rewrites a generated file can carry hundreds of thousands of
|
|
110
|
+
* lines; painting them all freezes the sidebar on a click. The header counts
|
|
111
|
+
* stay exact (they come from the parsed hunks, not from the rendered rows), so
|
|
112
|
+
* the truncation notice only claims what is actually missing: the paint.
|
|
113
|
+
*/
|
|
114
|
+
export const PATCH_ROW_CAP = 6000
|
|
115
|
+
|
|
116
|
+
/** Build the render model for one commit's `git show --patch` text. */
|
|
117
|
+
export function buildCommitPatch(text: string, rowCap = PATCH_ROW_CAP): CommitPatchModel {
|
|
118
|
+
const files: CommitPatchFile[] = []
|
|
119
|
+
let added = 0
|
|
120
|
+
let deleted = 0
|
|
121
|
+
let painted = 0
|
|
122
|
+
let truncated = false
|
|
123
|
+
|
|
124
|
+
for (const file of parseUnifiedDiff(text)) {
|
|
125
|
+
const rows: PatchRow[] = []
|
|
126
|
+
let fileAdded = 0
|
|
127
|
+
let fileDeleted = 0
|
|
128
|
+
let fileTruncated = false
|
|
129
|
+
/** End of the previous hunk, so the next one can name its jump. */
|
|
130
|
+
let oldEnd = 1
|
|
131
|
+
let newEnd = 1
|
|
132
|
+
for (const [index, hunk] of file.hunks.entries()) {
|
|
133
|
+
// The unshown gap before a hunk: git itself prints nothing there, but a
|
|
134
|
+
// reader of a unified patch needs to know the line numbers skipped.
|
|
135
|
+
const gap = hunk.newStart - newEnd
|
|
136
|
+
const oldGap = hunk.oldStart - oldEnd
|
|
137
|
+
if (index === 0 ? hunk.oldStart > 1 || hunk.newStart > 1 : gap > 0 || oldGap > 0) {
|
|
138
|
+
if (painted < rowCap) {
|
|
139
|
+
rows.push({ kind: 'skip', oldLine: null, newLine: null, text: '' })
|
|
140
|
+
painted += 1
|
|
141
|
+
} else {
|
|
142
|
+
fileTruncated = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let oldLine = hunk.oldStart
|
|
146
|
+
let newLine = hunk.newStart
|
|
147
|
+
for (const line of hunk.lines) {
|
|
148
|
+
if (line.startsWith('\\')) continue
|
|
149
|
+
const marker = line[0]
|
|
150
|
+
if (marker === '+') {
|
|
151
|
+
fileAdded += 1
|
|
152
|
+
if (painted < rowCap) {
|
|
153
|
+
rows.push({ kind: 'add', oldLine: null, newLine, text: line.slice(1) })
|
|
154
|
+
painted += 1
|
|
155
|
+
} else {
|
|
156
|
+
fileTruncated = true
|
|
157
|
+
}
|
|
158
|
+
newLine += 1
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (marker === '-') {
|
|
162
|
+
fileDeleted += 1
|
|
163
|
+
if (painted < rowCap) {
|
|
164
|
+
rows.push({ kind: 'del', oldLine, newLine: null, text: line.slice(1) })
|
|
165
|
+
painted += 1
|
|
166
|
+
} else {
|
|
167
|
+
fileTruncated = true
|
|
168
|
+
}
|
|
169
|
+
oldLine += 1
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (painted < rowCap) {
|
|
173
|
+
rows.push({ kind: 'context', oldLine, newLine, text: line.slice(1) })
|
|
174
|
+
painted += 1
|
|
175
|
+
} else {
|
|
176
|
+
fileTruncated = true
|
|
177
|
+
}
|
|
178
|
+
oldLine += 1
|
|
179
|
+
newLine += 1
|
|
180
|
+
}
|
|
181
|
+
oldEnd = oldLine
|
|
182
|
+
newEnd = newLine
|
|
183
|
+
}
|
|
184
|
+
added += fileAdded
|
|
185
|
+
deleted += fileDeleted
|
|
186
|
+
if (fileTruncated) truncated = true
|
|
187
|
+
files.push({
|
|
188
|
+
path: file.path,
|
|
189
|
+
oldPath: file.oldPath,
|
|
190
|
+
status: file.status,
|
|
191
|
+
binary: file.binary,
|
|
192
|
+
added: fileAdded,
|
|
193
|
+
deleted: fileDeleted,
|
|
194
|
+
rows,
|
|
195
|
+
truncated: fileTruncated,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { files, added, deleted, truncated }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The file block a path selects in the diff view (rename-aware). */
|
|
203
|
+
export function blockForPath(model: CommitPatchModel, path: string): string | undefined {
|
|
204
|
+
if (path === '') return undefined
|
|
205
|
+
for (const file of model.files) {
|
|
206
|
+
if (file.path === path || file.oldPath === path) return file.path
|
|
207
|
+
}
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Split one commit patch into its per-file raw texts, in the SAME order
|
|
213
|
+
* `buildCommitPatch` reports the files.
|
|
214
|
+
*
|
|
215
|
+
* Used by "copy this file's patch": the view still holds the original text, so
|
|
216
|
+
* the exact bytes git produced are what lands on the clipboard — no
|
|
217
|
+
* re-serialization from rows, which would drop headers a future git adds.
|
|
218
|
+
*/
|
|
219
|
+
export function splitCommitPatch(text: string): string[] {
|
|
220
|
+
const blocks: string[] = []
|
|
221
|
+
let current: string[] = []
|
|
222
|
+
for (const line of text.split('\n')) {
|
|
223
|
+
if (line.startsWith('diff --git ')) {
|
|
224
|
+
if (current.length > 0) blocks.push(joinBlock(current))
|
|
225
|
+
current = [line]
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (current.length > 0) current.push(line)
|
|
229
|
+
}
|
|
230
|
+
if (current.length > 0) blocks.push(joinBlock(current))
|
|
231
|
+
return blocks
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function joinBlock(lines: string[]): string {
|
|
235
|
+
const text = lines.join('\n')
|
|
236
|
+
return text.endsWith('\n') ? text : `${text}\n`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** One directory of the 内容 tree ('' = repository root). */
|
|
240
|
+
export interface CommitFileGroup {
|
|
241
|
+
dir: string
|
|
242
|
+
files: GitCommitFile[]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Group one commit's files by directory for the 内容 tree: a commit touching
|
|
247
|
+
* 40 files in 6 folders is a wall of paths as a flat list, and the folder row
|
|
248
|
+
* is also what lets a reader collapse the part they do not care about.
|
|
249
|
+
* Root-level files come first; every level keeps git's own order by path.
|
|
250
|
+
*/
|
|
251
|
+
export function groupCommitFiles(files: readonly GitCommitFile[]): CommitFileGroup[] {
|
|
252
|
+
const groups = new Map<string, GitCommitFile[]>()
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
const slash = file.path.lastIndexOf('/')
|
|
255
|
+
const dir = slash < 0 ? '' : file.path.slice(0, slash)
|
|
256
|
+
const list = groups.get(dir)
|
|
257
|
+
if (list === undefined) groups.set(dir, [file])
|
|
258
|
+
else list.push(file)
|
|
259
|
+
}
|
|
260
|
+
return [...groups.entries()]
|
|
261
|
+
.sort(([left], [right]) => {
|
|
262
|
+
if (left === right) return 0
|
|
263
|
+
if (left === '') return -1
|
|
264
|
+
if (right === '') return 1
|
|
265
|
+
return left.localeCompare(right)
|
|
266
|
+
})
|
|
267
|
+
.map(([dir, list]) => ({ dir, files: [...list].sort((a, b) => a.path.localeCompare(b.path)) }))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The next path in `paths` from `current` (wrapping at both ends), the diff
|
|
272
|
+
* toolbar's prev/next-file math. `current` unknown → the first/last path, so a
|
|
273
|
+
* fresh pane steps into the list instead of doing nothing.
|
|
274
|
+
*/
|
|
275
|
+
export function stepPath(paths: readonly string[], current: string | undefined, direction: 1 | -1): string | undefined {
|
|
276
|
+
if (paths.length === 0) return undefined
|
|
277
|
+
const index = current === undefined ? -1 : paths.indexOf(current)
|
|
278
|
+
if (index < 0) return direction === 1 ? paths[0] : paths[paths.length - 1]
|
|
279
|
+
return paths[(index + direction + paths.length) % paths.length]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Default folding for the 差异 tab: keep the first `rowBudget` rendered rows
|
|
284
|
+
* open, fold every file after that.
|
|
285
|
+
*
|
|
286
|
+
* A 40-file commit painting up to `PATCH_ROW_CAP` rows in a pane that may be
|
|
287
|
+
* 80px tall is a scroll wall where the reader sees none of the topology; the
|
|
288
|
+
* reference opens a curated subset for the same reason. Folding the TAIL (not
|
|
289
|
+
* per-file size) keeps the reading order intact: the files you scrolled to are
|
|
290
|
+
* still in the same place, just collapsed, and one click opens any of them.
|
|
291
|
+
*/
|
|
292
|
+
export function autoFoldCommitPatch(model: CommitPatchModel, rowBudget = 1500): string[] {
|
|
293
|
+
const folded: string[] = []
|
|
294
|
+
let painted = 0
|
|
295
|
+
for (const file of model.files) {
|
|
296
|
+
if (painted >= rowBudget) folded.push(file.path)
|
|
297
|
+
else painted += file.rows.length
|
|
298
|
+
}
|
|
299
|
+
return folded
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Ref badge decoration for one `%D` entry (shared by the graph rows and the
|
|
304
|
+
* commit card). Split for the same reason the history panel split them: the
|
|
305
|
+
* CSS modifier and the display text are different jobs.
|
|
306
|
+
*/
|
|
307
|
+
export function refBadgeClass(ref: string): string {
|
|
308
|
+
if (ref.startsWith('HEAD ->')) return ' head'
|
|
309
|
+
if (ref.startsWith('tag:')) return ' tag'
|
|
310
|
+
return ''
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Display text of one `%D` entry (`HEAD -> main` → `main`, `tag: v1` → `v1`). */
|
|
314
|
+
export function refBadgeLabel(ref: string): string {
|
|
315
|
+
if (ref.startsWith('HEAD -> ')) return ref.slice('HEAD -> '.length)
|
|
316
|
+
return ref.replace(/^tag: /, '')
|
|
317
|
+
}
|
package/src/client/store.tsx
CHANGED
|
@@ -350,7 +350,8 @@ const state: State = {
|
|
|
350
350
|
error: undefined,
|
|
351
351
|
hosts: [],
|
|
352
352
|
workspaces: [],
|
|
353
|
-
|
|
353
|
+
// Pre-bootstrap placeholder: same default as the plugin config (nforce).
|
|
354
|
+
routing: { mode: 'enforce', aliasPaths: {}, tombstones: [] },
|
|
354
355
|
workspaceId: readWorkspaceId(),
|
|
355
356
|
status: undefined,
|
|
356
357
|
head: undefined,
|
|
@@ -487,7 +488,7 @@ export interface ExplorerValue {
|
|
|
487
488
|
/** 停靠回主界面(底层的中心视图或侧栏表面)。 */
|
|
488
489
|
dockFloatingEditor: () => void
|
|
489
490
|
openDiff: (rel: string, staged: boolean, untracked?: boolean) => void
|
|
490
|
-
openCommitFile: (hash: string, rel: string) => void
|
|
491
|
+
openCommitFile: (hash: string, rel: string, origPath?: string) => void
|
|
491
492
|
activateTab: (id: string) => void
|
|
492
493
|
closeTab: (id: string) => void
|
|
493
494
|
updateTab: (id: string, patch: Partial<EditorTab>) => void
|
|
@@ -1872,13 +1873,19 @@ export function openDiff(rel: string, staged: boolean, untracked = false): void
|
|
|
1872
1873
|
})()
|
|
1873
1874
|
}
|
|
1874
1875
|
|
|
1875
|
-
/**
|
|
1876
|
-
|
|
1876
|
+
/**
|
|
1877
|
+
* Open (or focus) one file of one commit (`ref^` against `ref`).
|
|
1878
|
+
*
|
|
1879
|
+
* `origPath` is the rename source from the commit's file row / patch header:
|
|
1880
|
+
* the two-sided view must diff the OLD path against the NEW one, or git sees
|
|
1881
|
+
* two unrelated paths and paints the rename as a whole-file addition.
|
|
1882
|
+
*/
|
|
1883
|
+
export function openCommitFile(hash: string, rel: string, origPath?: string): void {
|
|
1877
1884
|
captureLiveBuffer()
|
|
1878
1885
|
const workspace = currentWorkspace()
|
|
1879
1886
|
const root = state.repoRoot
|
|
1880
1887
|
if (workspace === undefined || root === undefined || rel === '') return
|
|
1881
|
-
const id = `commit:${workspace.alias}:${hash}:${rel}`
|
|
1888
|
+
const id = `commit:${workspace.alias}:${hash}:${rel}${origPath === undefined ? '' : `:<-${origPath}`}`
|
|
1882
1889
|
upsertTab({
|
|
1883
1890
|
id,
|
|
1884
1891
|
kind: 'diff',
|
|
@@ -1904,7 +1911,7 @@ export function openCommitFile(hash: string, rel: string): void {
|
|
|
1904
1911
|
})
|
|
1905
1912
|
void (async () => {
|
|
1906
1913
|
try {
|
|
1907
|
-
const payload = await withDeadline(loadCommitFileDiff(workspace.alias, root, hash, rel), gitDeadlineMs(), t('editor.readTimeout'))
|
|
1914
|
+
const payload = await withDeadline(loadCommitFileDiff(workspace.alias, root, hash, rel, origPath), gitDeadlineMs(), t('editor.readTimeout'))
|
|
1908
1915
|
patchTab(id, {
|
|
1909
1916
|
loading: false,
|
|
1910
1917
|
diff: payload,
|
package/src/client/vsc/kit.tsx
CHANGED
|
@@ -51,6 +51,42 @@ export function IconChevronRight(props: IconProps): ReactNode {
|
|
|
51
51
|
return svg(props.size ?? 14, <path d="M6 3.5 L10.5 8 L6 12.5" />, props.className)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Mirror of {@link IconChevronRight}, for the commit pane's "previous file". */
|
|
55
|
+
export function IconChevronLeft(props: IconProps): ReactNode {
|
|
56
|
+
return svg(props.size ?? 14, <path d="M10 3.5 L5.5 8 L10 12.5" />, props.className)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Expand: two arrows pushing the corners apart (the pane's full-height toggle). */
|
|
60
|
+
export function IconExpand(props: IconProps): ReactNode {
|
|
61
|
+
return svg(props.size ?? 13, (
|
|
62
|
+
<>
|
|
63
|
+
<path d="M9.4 2.6h4v4M13.4 2.6 9 7" />
|
|
64
|
+
<path d="M6.6 13.4h-4v-4M2.6 13.4 7 9" />
|
|
65
|
+
</>
|
|
66
|
+
), props.className)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Restore: the same two arrows pulling the corners back in. */
|
|
70
|
+
export function IconCollapse(props: IconProps): ReactNode {
|
|
71
|
+
return svg(props.size ?? 13, (
|
|
72
|
+
<>
|
|
73
|
+
<path d="M13.4 6.6h-4v-4M9.4 6.6 13.4 2.6" />
|
|
74
|
+
<path d="M2.6 9.4h4v4M6.6 9.4 2.6 13.4" />
|
|
75
|
+
</>
|
|
76
|
+
), props.className)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Wrap long lines: a line turning back on itself instead of running off. */
|
|
80
|
+
export function IconWrap(props: IconProps): ReactNode {
|
|
81
|
+
return svg(props.size ?? 13, (
|
|
82
|
+
<>
|
|
83
|
+
<path d="M2.6 4.4h8.6a2.6 2.6 0 0 1 0 5.2H6.4" />
|
|
84
|
+
<path d="M8.2 7.4 6.2 9.6l2 2.2" />
|
|
85
|
+
<path d="M2.6 11.6h1.8" />
|
|
86
|
+
</>
|
|
87
|
+
), props.className)
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
export function IconChevronDown(props: IconProps): ReactNode {
|
|
55
91
|
return svg(props.size ?? 14, <path d="M3.5 6 L8 10.5 L12.5 6" />, props.className)
|
|
56
92
|
}
|
|
@@ -90,6 +126,41 @@ export function IconClose(props: IconProps): ReactNode {
|
|
|
90
126
|
return svg(props.size ?? 12, <path d="M4 4l8 8M12 4l-8 8" />, props.className)
|
|
91
127
|
}
|
|
92
128
|
|
|
129
|
+
/** Stop: a filled square — the universal "kill this run" mark. */
|
|
130
|
+
export function IconStop(props: IconProps): ReactNode {
|
|
131
|
+
return svg(props.size ?? 12, <rect x="4.2" y="4.2" width="7.6" height="7.6" rx="1.2" fill="currentColor" stroke="none" />, props.className)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Copy glyph: two offset sheets, the front one the clipboard's payload. */
|
|
135
|
+
export function IconCopy(props: IconProps): ReactNode {
|
|
136
|
+
return svg(props.size ?? 14, (
|
|
137
|
+
<>
|
|
138
|
+
<rect x="5.4" y="5.4" width="7.8" height="7.8" rx="1.2" />
|
|
139
|
+
<path d="M10.6 5.4V3.6a1.2 1.2 0 0 0-1.2-1.2H3.6a1.2 1.2 0 0 0-1.2 1.2v5.8a1.2 1.2 0 0 0 1.2 1.2h1.8" />
|
|
140
|
+
</>
|
|
141
|
+
), props.className)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Author glyph for the commit pane's metadata rows. */
|
|
145
|
+
export function IconUser(props: IconProps): ReactNode {
|
|
146
|
+
return svg(props.size ?? 13, (
|
|
147
|
+
<>
|
|
148
|
+
<circle cx="8" cy="5.4" r="2.6" />
|
|
149
|
+
<path d="M2.8 13.4c.5-2.4 2.6-3.8 5.2-3.8s4.7 1.4 5.2 3.8" />
|
|
150
|
+
</>
|
|
151
|
+
), props.className)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Date glyph, the pair of {@link IconUser} in the commit pane's metadata. */
|
|
155
|
+
export function IconClock(props: IconProps): ReactNode {
|
|
156
|
+
return svg(props.size ?? 13, (
|
|
157
|
+
<>
|
|
158
|
+
<circle cx="8" cy="8" r="5.6" />
|
|
159
|
+
<path d="M8 4.8V8l2.2 1.6" />
|
|
160
|
+
</>
|
|
161
|
+
), props.className)
|
|
162
|
+
}
|
|
163
|
+
|
|
93
164
|
export function IconSearch(props: IconProps): ReactNode {
|
|
94
165
|
return svg(props.size ?? 16, <path d="M10.6 10.6 14 14M11.8 7.2a4.6 4.6 0 1 1-9.2 0 4.6 4.6 0 0 1 9.2 0Z" />, props.className)
|
|
95
166
|
}
|