dsh-ide-git 0.13.4 → 0.13.5
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.md +16 -3
- package/README_EN.md +15 -2
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +586 -15
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
|
|
|
13
13
|
- **分支管理没有树**:内置面板只有一个分支下拉。这里给出 `HEAD(当前分支) / 本地 / 远程 / 标签` 的分组树,每个分支带 ahead/behind 角标、上游、worktree 占用标记(`W`)。
|
|
14
14
|
- **历史只是列表**:这里画出提交图谱(lane 拓扑 + 分支/标签 ref 徽章),支持加载更多与过滤(文本 / 哈希 / 作者)。
|
|
15
15
|
- **想看某个提交改了什么**:点提交直接进详情——作者、日期、完整提交信息、变更文件(含 +/− 行数),点文件看逐行 diff(红绿 + 行号 + hunk 高亮)。
|
|
16
|
+
- **想指给 AI 一段代码**:在 diff 窗里选中几行、右键,把「哪个文件哪几行」(附提交或工作区上下文)直接发进当前会话的输入框。
|
|
16
17
|
- **想比较分支**:分支右键即可与当前分支比较,结果按文件与提交数汇报。
|
|
17
18
|
- **操作要够全**:签出、从任意分支/提交新建分支、重命名、删除、合并到当前、变基当前到此、新建标签、cherry-pick、还原提交、重置(保留/丢弃)、fetch / pull --ff-only / push(确认后)。
|
|
18
19
|
|
|
@@ -56,6 +57,17 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
|
|
|
56
57
|
</tr>
|
|
57
58
|
</table>
|
|
58
59
|
|
|
60
|
+
## 右键发送到对话
|
|
61
|
+
|
|
62
|
+
面板里的一切都能右键塞进**当前会话的输入框**——走 DSH 官方的 composer 插入通道(不动你已输入的文字,追加在末尾;发送中或服务不在时自动退化为复制到剪贴板并提示,绝不死点击):
|
|
63
|
+
|
|
64
|
+
- **提交** → `git 提交 47ad017「提交标题」(仓库 xxx)`:短哈希 + 提交信息 + 仓库,一段话指名一个修订;
|
|
65
|
+
- **分支** → `git 分支 main(仓库 xxx)`;
|
|
66
|
+
- **文件**(变更列表与提交详情的文件行)→ 一个 `@路径` 文件引用 chip(DSH 原生 @ 语法,含空格的路径自动加引号),或者只发文件路径;
|
|
67
|
+
- **diff 选中行**:在提交详情或工作区变更打开的 diff 窗里**选中一段文本再右键** → 「发送选中行到对话」:`@文件` chip + 行号说明——提交场景是「第 N–M 行(提交 47ad017「标题」@ 当前分支)」,工作区场景是「第 N–M 行(工作区已暂存 / 未提交)」。行号由选区所在的 diff 行按 hunk 头换算成**新文件行号**;选中的全是删除行时用旧号并标注「旧行号」。选区落不到可定位的 diff 行上时,菜单退化为「发送选中片段到对话」(原文截断 600 字符),不编造行号;菜单里同时有「复制选中引用」。没有选区时右键保持浏览器默认菜单。
|
|
68
|
+
|
|
69
|
+
配套已经就位的几件事:变更文件行与详情文件行的行首图标按扩展名着色(DSH 自家 FileTypeIcon);diff 预览用 DSH 自家 DiffBlock 组件渲染(红绿底、复制 / 换行工具栏,视觉与宿主一致);diff 窗高度可拖拽调节并按位置记忆;整个面板还可以挂进**主对话区**——「对话 / 轨迹」旁的 Git 标签页,插件设置里可关。
|
|
70
|
+
|
|
59
71
|
## 挂在哪里:两个宿主,一个面板
|
|
60
72
|
|
|
61
73
|
本插件只注册一次,落到哪个面上由**宿主**决定,两条通道互斥:
|
|
@@ -84,14 +96,15 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
|
|
|
84
96
|
| 动作条 | IDEA 式左侧竖排(底部工作台)/ 顶栏下一行横排(右侧栏):刷新、新建分支、签出、删除、比较、显示差异、贮藏、新建标签、收藏、抓取、拉取、推送。**按可用空间自动决定放几个**,放不下的收进 `⋯ 更多`;末尾 `⚙ 设置` 可拖动排序、逐项显隐(存 `dsh-ide-git.rail.v1`)。每个按钮按当前状态点亮/置灰(没有其他分支就不能删除/签出/比较,没有远程就不能抓取/推送,没有上游就不能拉取,没有变更就没有差异可看) |
|
|
85
97
|
| 状态行 | 当前分支、上游、`↑ahead ↓behind`、stash 数量、忙碌指示 |
|
|
86
98
|
| 分支树 | `HEAD / 本地 / 远程 / 标签` 分组(可折叠)、过滤框、星标当前分支、ahead/behind 角标、worktree 占用标记;本地蓝 / 远程紫 / 标签黄 / HEAD 绿 |
|
|
87
|
-
| 分支右键 | 签出、将当前分支变基到此、合并到当前分支、与当前分支比较、从该分支新建分支、重命名、删除(当前分支禁用)、更新(Fetch)
|
|
99
|
+
| 分支右键 | 签出、将当前分支变基到此、合并到当前分支、与当前分支比较、从该分支新建分支、重命名、删除(当前分支禁用)、更新(Fetch)、推送、发送到对话(分支名 + 仓库) |
|
|
88
100
|
| 历史筛选 | 文本或哈希、分支或标签、提交人、日期(今天 / 近 7 天 / 近 30 天 / 今年),外加排序方向(新→旧 / 旧→新)与一键清除;路径筛选回车后由 git 服务端过滤(`git log -- <path>`) |
|
|
89
101
|
| 提交列表 | 列序对齐 IDEA:**日期 → 提交人 → 图谱 → 分支标签 → 提交信息**;lane 拓扑图整格连线(相邻行像素对齐)、HEAD 空心点、refs 徽章(HEAD 绿 / 本地蓝 / 远程紫 / 标签黄,最多 3 个 + `+n`)、加载更多(每页 120) |
|
|
90
|
-
| 提交右键 | 提交详情、复制修订号、签出该修订(确认)、在此新建分支、新建标签、cherry-pick、还原提交、重置到此(保留 / 丢弃更改) |
|
|
102
|
+
| 提交右键 | 提交详情、复制修订号、签出该修订(确认)、在此新建分支、新建标签、cherry-pick、还原提交、重置到此(保留 / 丢弃更改)、发送到对话(短哈希 + 提交信息 + 仓库) |
|
|
91
103
|
| 提交详情 | 完整提交信息、作者/日期、变更文件列表(+/−),点文件看逐行 diff |
|
|
104
|
+
| diff 窗 | DSH 自家 DiffBlock 渲染(红绿底 + 复制/换行工具栏)、高度可拖拽调节并按位置记忆;**选中 diff 行右键**把行级引用发进对话——`@文件` + 第 N–M 行 + 提交(短哈希/标题/当前分支)或工作区(已暂存/未提交)上下文,删除行标注旧行号,选区定位不到行时发选中片段(截断 600 字符),另有「复制选中引用」 |
|
|
92
105
|
| 变更列表 | 冲突 / 已暂存 / 更改 / 未跟踪 四组,行内 暂存 / 取消暂存 / 丢弃,分组级「全部暂存 / 全部取消暂存」,点击行看 diff |
|
|
93
106
|
| 提交框 | 多行提交信息、`Ctrl+Enter`(macOS `Cmd+Enter`)提交、修补上次提交(`--amend`) |
|
|
94
|
-
| 文件右键 |
|
|
107
|
+
| 文件右键 | 显示差异、暂存/取消暂存、丢弃更改、复制路径、发送到对话(`@文件` 引用 chip 或文件路径) |
|
|
95
108
|
| 安全与撤回 | 删除分支 / 删除贮藏后右下角弹提示条,点「撤回」即可恢复(真删除 + 按删除前记下的对象 id 重建,一次性、30 分钟内有效),顶栏的撤回图标里也能找到最近删除;`main`/`master`/`trunk` 的删除要求输入分支名确认,所有破坏性确认框焦点默认在「取消」;同一仓库的写操作在宿主串行执行;仓库里还有未结束的 merge / rebase / cherry-pick / revert / bisect 时,相关动作全部置灰并说明原因 |
|
|
96
109
|
|
|
97
110
|
## 安装
|
package/README_EN.md
CHANGED
|
@@ -48,6 +48,17 @@ DSH's built-in Git panel covers stage / commit / revert / history. `dsh-ide-git`
|
|
|
48
48
|
</tr>
|
|
49
49
|
</table>
|
|
50
50
|
|
|
51
|
+
## Send to chat
|
|
52
|
+
|
|
53
|
+
Everything in the panel can be right-clicked into the **current session's composer** — through DSH's official insertion channel (it appends at the tail without touching what you already typed; while sending, or when the service is absent, it falls back to copying the payload with a toast, never a dead click):
|
|
54
|
+
|
|
55
|
+
- **Commit** → `git commit 47ad017 "Subject" (repo xxx)`: short hash + message + repository, one sentence naming a revision;
|
|
56
|
+
- **Branch** → `git branch main (repo xxx)`;
|
|
57
|
+
- **File** (change rows and commit-detail file rows) → a `@path` file reference chip (DSH's native @ grammar, quoted paths included) or just the raw path;
|
|
58
|
+
- **Selected diff lines**: select some text in a diff window (opened from a commit detail or the working tree), right-click → **Send selected lines to chat**: an `@file` chip plus a line note — `lines N–M (commit 47ad017 "Subject" @ branch)` for a commit, `lines N–M (worktree, staged / uncommitted)` for the working tree. Numbers are reconstructed from the diff rows under the selection against the patch's hunk headers, in **new-file numbering**; a purely deleted span reports OLD numbers and says so. If the selection cannot be pinned to diff rows the menu degrades to **Send selected snippet to chat** (text truncated at 600 characters) instead of inventing numbers, and a **Copy the selection reference** item is always there. With no selection the browser's own menu stays.
|
|
59
|
+
|
|
60
|
+
Also in place: change-row and detail-file-row icons are tinted by extension (DSH's own FileTypeIcon); diff previews render through DSH's own DiffBlock (red/green rows, copy / wrap toolbar, host-consistent visuals); the diff window's height is draggable and remembered per placement; and the whole panel can live in the **main conversation area** — the Git tab next to Dialogue / Trace, switchable in the plugin settings.
|
|
61
|
+
|
|
51
62
|
## Where it mounts: two hosts, one panel
|
|
52
63
|
|
|
53
64
|
The plugin registers once; which surface it lands on is the **host's** decision, and the two doors are exclusive:
|
|
@@ -74,13 +85,15 @@ The panel never guesses: it measures itself with a `ResizeObserver` (width ≥ 6
|
|
|
74
85
|
- Action rail: the JetBrains-style vertical strip in the bottom workbench (and one horizontal row under the header in the right sidebar) with refresh, new branch, checkout, delete, compare, show diff, stash, new tag, favorite, fetch, pull and push. **It shows as many buttons as fit**, folds the rest into a `⋯ more` menu, and pins a `⚙ settings` button that lets you reorder actions and toggle each of them (stored in `dsh-ide-git.rail.v1`). Every action lights up or greys out with the current state (no other branch → delete / checkout / compare off, no remote → fetch / push off, no upstream → pull off, no changes → diff off).
|
|
75
86
|
- Status line: branch, upstream, ahead/behind, stash count, busy indicator.
|
|
76
87
|
- Branch tree: HEAD / Local / Remote / Tags groups, filter box, ahead/behind badges, worktree marker; local blue / remote violet / tag amber / HEAD green.
|
|
77
|
-
- Branch context menu: checkout, rebase current onto this, merge into current, compare with current, new branch from here, rename, delete, fetch, push.
|
|
88
|
+
- Branch context menu: checkout, rebase current onto this, merge into current, compare with current, new branch from here, rename, delete, fetch, push, send to chat (branch + repo).
|
|
78
89
|
- History filters: text or hash, branch or tag, author, date (today / 7 days / 30 days / this year), an order toggle (newest / oldest first) and one-click clear; the path filter runs server-side (`git log -- <path>`) on Enter.
|
|
79
90
|
- Commit list: IDEA column order — **date → author → graph → refs → subject**; a lane graph whose segments span whole rows (pixel-aligned across rows), a hollow dot for HEAD, refs badges (HEAD green / local blue / remote violet / tag amber, at most three plus `+n`), load more (120 per page).
|
|
80
|
-
- Commit context menu: details, copy revision, checkout revision, new branch here, new tag, cherry-pick, revert, reset here (keep / discard changes).
|
|
91
|
+
- Commit context menu: details, copy revision, checkout revision, new branch here, new tag, cherry-pick, revert, reset here (keep / discard changes), send to chat (short hash + message + repo).
|
|
81
92
|
- Commit details: full message, author/date, changed files with +/− and per-file line diff.
|
|
93
|
+
- Diff pane: rendered with DSH's own DiffBlock (red/green rows plus a copy / wrap toolbar), draggable height remembered per placement; **select diff rows and right-click** to send a line-level reference into the conversation — an `@file` chip plus the line range with commit (hash / subject / current branch) or worktree (staged / uncommitted) context; deleted rows are labelled as old-numbered, selections that cannot be pinned to rows degrade to the raw snippet (truncated at 600 characters), and a copy-reference item is always available.
|
|
82
94
|
- Changes: conflicts / staged / changes / untracked groups, inline stage / unstage / discard, group-level actions, click for diff.
|
|
83
95
|
- Commit box: multi-line message, Ctrl+Enter (Cmd+Enter) to commit, amend.
|
|
96
|
+
- File context menu: show diff, stage / unstage, discard changes, copy path, send to chat (an `@file` reference chip or the raw path).
|
|
84
97
|
- **Safety and undo**: deleting a branch or a stash pops a toast in the corner with an **Undo** action — the delete is real, and undo recreates the ref from the object id recorded just before it (one-shot, valid for 30 minutes). Deleting `main` / `master` / `trunk` requires typing the branch name, destructive dialogs focus Cancel instead of the red button, writes are serialised per repository on the host, and while a merge / rebase / cherry-pick / revert / bisect is still open the affected actions are greyed out with the reason.
|
|
85
98
|
|
|
86
99
|
## Install
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-ide-git",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ide-git",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/client.js
CHANGED
|
@@ -156,6 +156,15 @@ window.__ModuleLoader__.load({
|
|
|
156
156
|
'menu.sendFilePathToChat': '发送文件路径到对话',
|
|
157
157
|
'send.commitText': 'git 提交 {hash}「{subject}」(仓库 {repo})',
|
|
158
158
|
'send.branchText': 'git 分支 {name}(仓库 {repo})',
|
|
159
|
+
'menu.sendDiffLinesToChat': '发送选中行到对话',
|
|
160
|
+
'menu.sendDiffSnippetToChat': '发送选中片段到对话',
|
|
161
|
+
'menu.copyDiffRef': '复制选中引用',
|
|
162
|
+
'send.diffLinesCommit': '{lines}(提交 {hash}「{subject}」@ {branch})',
|
|
163
|
+
'send.diffLinesWorktree': '{lines}(工作区{state})',
|
|
164
|
+
'send.diffStaged': '已暂存',
|
|
165
|
+
'send.diffUnstaged': '未提交',
|
|
166
|
+
'send.diffNewLines': '第 {lines} 行',
|
|
167
|
+
'send.diffOldLines': '第 {lines} 行(旧行号)',
|
|
159
168
|
'history.empty': '这个仓库还没有提交',
|
|
160
169
|
'history.loadMore': '加载更多',
|
|
161
170
|
'history.filter': '文本或哈希',
|
|
@@ -358,6 +367,15 @@ window.__ModuleLoader__.load({
|
|
|
358
367
|
'menu.sendFilePathToChat': 'Send file path to chat',
|
|
359
368
|
'send.commitText': 'git commit {hash} "{subject}" (repo {repo})',
|
|
360
369
|
'send.branchText': 'git branch {name} (repo {repo})',
|
|
370
|
+
'menu.sendDiffLinesToChat': 'Send selected lines to chat',
|
|
371
|
+
'menu.sendDiffSnippetToChat': 'Send selected snippet to chat',
|
|
372
|
+
'menu.copyDiffRef': 'Copy the selection reference',
|
|
373
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
374
|
+
'send.diffLinesWorktree': '{lines} (worktree, {state})',
|
|
375
|
+
'send.diffStaged': 'staged',
|
|
376
|
+
'send.diffUnstaged': 'uncommitted',
|
|
377
|
+
'send.diffNewLines': 'lines {lines}',
|
|
378
|
+
'send.diffOldLines': 'lines {lines} (old numbering)',
|
|
361
379
|
'history.empty': 'This repository has no commits yet',
|
|
362
380
|
'history.loadMore': 'Load more',
|
|
363
381
|
'history.filter': 'Text or hash',
|
|
@@ -568,6 +586,15 @@ window.__ModuleLoader__.load({
|
|
|
568
586
|
'menu.sendFilePathToChat': '傳送檔案路徑至對話',
|
|
569
587
|
'send.commitText': 'git 提交 {hash}「{subject}」(存放庫 {repo})',
|
|
570
588
|
'send.branchText': 'git 分支 {name}(存放庫 {repo})',
|
|
589
|
+
'menu.sendDiffLinesToChat': '傳送選中行到對話',
|
|
590
|
+
'menu.sendDiffSnippetToChat': '傳送選中片段到對話',
|
|
591
|
+
'menu.copyDiffRef': '複製選中引用',
|
|
592
|
+
'send.diffLinesCommit': '{lines}(提交 {hash}「{subject}」@ {branch})',
|
|
593
|
+
'send.diffLinesWorktree': '{lines}(工作區{state})',
|
|
594
|
+
'send.diffStaged': '已暫存',
|
|
595
|
+
'send.diffUnstaged': '未提交',
|
|
596
|
+
'send.diffNewLines': '第 {lines} 行',
|
|
597
|
+
'send.diffOldLines': '第 {lines} 行(舊行號)',
|
|
571
598
|
'history.empty': '呢個存放庫仲未有提交',
|
|
572
599
|
'history.loadMore': '載入更多',
|
|
573
600
|
'history.filter': '文字或雜湊',
|
|
@@ -770,6 +797,15 @@ window.__ModuleLoader__.load({
|
|
|
770
797
|
'menu.sendFilePathToChat': '傳送檔案路徑至對話',
|
|
771
798
|
'send.commitText': 'git 提交 {hash}「{subject}」(版本庫 {repo})',
|
|
772
799
|
'send.branchText': 'git 分支 {name}(版本庫 {repo})',
|
|
800
|
+
'menu.sendDiffLinesToChat': '傳送選取行至對話',
|
|
801
|
+
'menu.sendDiffSnippetToChat': '傳送選取片段至對話',
|
|
802
|
+
'menu.copyDiffRef': '複製選取的引用',
|
|
803
|
+
'send.diffLinesCommit': '{lines}(提交 {hash}「{subject}」@ {branch})',
|
|
804
|
+
'send.diffLinesWorktree': '{lines}(工作區{state})',
|
|
805
|
+
'send.diffStaged': '已暫存',
|
|
806
|
+
'send.diffUnstaged': '未提交',
|
|
807
|
+
'send.diffNewLines': '第 {lines} 行',
|
|
808
|
+
'send.diffOldLines': '第 {lines} 行(舊行號)',
|
|
773
809
|
'history.empty': '這個版本庫還沒有提交',
|
|
774
810
|
'history.loadMore': '載入更多',
|
|
775
811
|
'history.filter': '文字或雜湊',
|
|
@@ -972,6 +1008,15 @@ window.__ModuleLoader__.load({
|
|
|
972
1008
|
'menu.sendFilePathToChat': '傳送檔案路徑至對話',
|
|
973
1009
|
'send.commitText': 'git 提交 {hash}「{subject}」(存放庫 {repo})',
|
|
974
1010
|
'send.branchText': 'git 分支 {name}(存放庫 {repo})',
|
|
1011
|
+
'menu.sendDiffLinesToChat': '傳送選中行至對話',
|
|
1012
|
+
'menu.sendDiffSnippetToChat': '傳送選中片段至對話',
|
|
1013
|
+
'menu.copyDiffRef': '複製選中引用',
|
|
1014
|
+
'send.diffLinesCommit': '{lines}(提交 {hash}「{subject}」@ {branch})',
|
|
1015
|
+
'send.diffLinesWorktree': '{lines}(工作區{state})',
|
|
1016
|
+
'send.diffStaged': '已暫存',
|
|
1017
|
+
'send.diffUnstaged': '未提交',
|
|
1018
|
+
'send.diffNewLines': '第 {lines} 行',
|
|
1019
|
+
'send.diffOldLines': '第 {lines} 行(舊行號)',
|
|
975
1020
|
'history.empty': '呢個存放庫仲未有提交',
|
|
976
1021
|
'history.loadMore': '載入更多',
|
|
977
1022
|
'history.filter': '文字或雜湊',
|
|
@@ -1174,6 +1219,15 @@ window.__ModuleLoader__.load({
|
|
|
1174
1219
|
'menu.sendFilePathToChat': 'ファイルパスをチャットへ送る',
|
|
1175
1220
|
'send.commitText': 'git コミット {hash}「{subject}」(リポジトリ {repo})',
|
|
1176
1221
|
'send.branchText': 'git ブランチ {name}(リポジトリ {repo})',
|
|
1222
|
+
'menu.sendDiffLinesToChat': '選択した行をチャットへ送る',
|
|
1223
|
+
'menu.sendDiffSnippetToChat': '選択した範囲をチャットへ送る',
|
|
1224
|
+
'menu.copyDiffRef': '選択範囲の参照をコピー',
|
|
1225
|
+
'send.diffLinesCommit': '{lines}(コミット {hash}「{subject}」@ {branch})',
|
|
1226
|
+
'send.diffLinesWorktree': '{lines}(作業ツリー、{state})',
|
|
1227
|
+
'send.diffStaged': 'ステージ済み',
|
|
1228
|
+
'send.diffUnstaged': '未コミット',
|
|
1229
|
+
'send.diffNewLines': '{lines} 行目',
|
|
1230
|
+
'send.diffOldLines': '{lines} 行目(旧番号)',
|
|
1177
1231
|
'history.empty': 'このリポジトリにはまだコミットがありません',
|
|
1178
1232
|
'history.loadMore': 'さらに読み込む',
|
|
1179
1233
|
'history.filter': 'テキストまたはハッシュ',
|
|
@@ -1376,6 +1430,15 @@ window.__ModuleLoader__.load({
|
|
|
1376
1430
|
'menu.sendFilePathToChat': '파일 경로를 대화로 보내기',
|
|
1377
1431
|
'send.commitText': 'git 커밋 {hash} "{subject}" (저장소 {repo})',
|
|
1378
1432
|
'send.branchText': 'git 브랜치 {name} (저장소 {repo})',
|
|
1433
|
+
'menu.sendDiffLinesToChat': '선택한 줄 대화로 보내기',
|
|
1434
|
+
'menu.sendDiffSnippetToChat': '선택한 조각 대화로 보내기',
|
|
1435
|
+
'menu.copyDiffRef': '선택 범위 참조 복사',
|
|
1436
|
+
'send.diffLinesCommit': '{lines}(커밋 {hash} "{subject}" @ {branch})',
|
|
1437
|
+
'send.diffLinesWorktree': '{lines}(작업 트리, {state})',
|
|
1438
|
+
'send.diffStaged': '스테이징됨',
|
|
1439
|
+
'send.diffUnstaged': '커밋되지 않음',
|
|
1440
|
+
'send.diffNewLines': '{lines}행',
|
|
1441
|
+
'send.diffOldLines': '{lines}행(이전 번호)',
|
|
1379
1442
|
'history.empty': '이 저장소에는 아직 커밋이 없습니다',
|
|
1380
1443
|
'history.loadMore': '더 불러오기',
|
|
1381
1444
|
'history.filter': '텍스트 또는 해시',
|
|
@@ -1578,6 +1641,15 @@ window.__ModuleLoader__.load({
|
|
|
1578
1641
|
'menu.sendFilePathToChat': 'Dateipfad im Chat senden',
|
|
1579
1642
|
'send.commitText': 'git-Commit {hash} "{subject}" (Repository {repo})',
|
|
1580
1643
|
'send.branchText': 'git-Branch {name} (Repository {repo})',
|
|
1644
|
+
'menu.sendDiffLinesToChat': 'Ausgewählte Zeilen in den Chat senden',
|
|
1645
|
+
'menu.sendDiffSnippetToChat': 'Ausgewählten Ausschnitt in den Chat senden',
|
|
1646
|
+
'menu.copyDiffRef': 'Auswahlbezug kopieren',
|
|
1647
|
+
'send.diffLinesCommit': '{lines} (Commit {hash} "{subject}" @ {branch})',
|
|
1648
|
+
'send.diffLinesWorktree': '{lines} (Arbeitsbaum, {state})',
|
|
1649
|
+
'send.diffStaged': 'staged',
|
|
1650
|
+
'send.diffUnstaged': 'nicht committet',
|
|
1651
|
+
'send.diffNewLines': 'Zeilen {lines}',
|
|
1652
|
+
'send.diffOldLines': 'Zeilen {lines} (alte Nummerierung)',
|
|
1581
1653
|
'history.empty': 'Dieses Repository hat noch keine Commits',
|
|
1582
1654
|
'history.loadMore': 'Mehr laden',
|
|
1583
1655
|
'history.filter': 'Text oder Hash',
|
|
@@ -1780,6 +1852,15 @@ window.__ModuleLoader__.load({
|
|
|
1780
1852
|
'menu.sendFilePathToChat': 'Envoyer le chemin du fichier dans la conversation',
|
|
1781
1853
|
'send.commitText': 'commit git {hash} « {subject} » (dépôt {repo})',
|
|
1782
1854
|
'send.branchText': 'branche git {name} (dépôt {repo})',
|
|
1855
|
+
'menu.sendDiffLinesToChat': 'Envoyer les lignes sélectionnées dans la conversation',
|
|
1856
|
+
'menu.sendDiffSnippetToChat': 'Envoyer l\'extrait sélectionné dans la conversation',
|
|
1857
|
+
'menu.copyDiffRef': 'Copier la référence de la sélection',
|
|
1858
|
+
'send.diffLinesCommit': '{lines} (commit {hash} « {subject} » @ {branch})',
|
|
1859
|
+
'send.diffLinesWorktree': '{lines} (arbre de travail, {state})',
|
|
1860
|
+
'send.diffStaged': 'indexé',
|
|
1861
|
+
'send.diffUnstaged': 'non commité',
|
|
1862
|
+
'send.diffNewLines': 'lignes {lines}',
|
|
1863
|
+
'send.diffOldLines': 'lignes {lines} (ancienne numérotation)',
|
|
1783
1864
|
'history.empty': 'Ce dépôt n\'a pas encore de commit',
|
|
1784
1865
|
'history.loadMore': 'Charger plus',
|
|
1785
1866
|
'history.filter': 'Texte ou empreinte',
|
|
@@ -1982,6 +2063,15 @@ window.__ModuleLoader__.load({
|
|
|
1982
2063
|
'menu.sendFilePathToChat': 'Отправить путь к файлу в чат',
|
|
1983
2064
|
'send.commitText': 'git-коммит {hash} «{subject}» (репозиторий {repo})',
|
|
1984
2065
|
'send.branchText': 'git-ветка {name} (репозиторий {repo})',
|
|
2066
|
+
'menu.sendDiffLinesToChat': 'Отправить выбранные строки в чат',
|
|
2067
|
+
'menu.sendDiffSnippetToChat': 'Отправить выбранный фрагмент в чат',
|
|
2068
|
+
'menu.copyDiffRef': 'Скопировать ссылку на выделение',
|
|
2069
|
+
'send.diffLinesCommit': '{lines} (коммит {hash} «{subject}» @ {branch})',
|
|
2070
|
+
'send.diffLinesWorktree': '{lines} (рабочее дерево, {state})',
|
|
2071
|
+
'send.diffStaged': 'в индексе',
|
|
2072
|
+
'send.diffUnstaged': 'не закоммичено',
|
|
2073
|
+
'send.diffNewLines': 'строки {lines}',
|
|
2074
|
+
'send.diffOldLines': 'строки {lines} (старая нумерация)',
|
|
1985
2075
|
'history.empty': 'В этом репозитории ещё нет коммитов',
|
|
1986
2076
|
'history.loadMore': 'Загрузить ещё',
|
|
1987
2077
|
'history.filter': 'Текст или хеш',
|
|
@@ -2184,6 +2274,15 @@ window.__ModuleLoader__.load({
|
|
|
2184
2274
|
'menu.sendFilePathToChat': 'Enviar caminho do arquivo para a conversa',
|
|
2185
2275
|
'send.commitText': 'commit git {hash} "{subject}" (repositório {repo})',
|
|
2186
2276
|
'send.branchText': 'branch git {name} (repositório {repo})',
|
|
2277
|
+
'menu.sendDiffLinesToChat': 'Enviar linhas selecionadas para a conversa',
|
|
2278
|
+
'menu.sendDiffSnippetToChat': 'Enviar trecho selecionado para a conversa',
|
|
2279
|
+
'menu.copyDiffRef': 'Copiar a referência da seleção',
|
|
2280
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
2281
|
+
'send.diffLinesWorktree': '{lines} (árvore de trabalho, {state})',
|
|
2282
|
+
'send.diffStaged': 'preparado',
|
|
2283
|
+
'send.diffUnstaged': 'não commitado',
|
|
2284
|
+
'send.diffNewLines': 'linhas {lines}',
|
|
2285
|
+
'send.diffOldLines': 'linhas {lines} (numeração antiga)',
|
|
2187
2286
|
'history.empty': 'Este repositório ainda não tem commits',
|
|
2188
2287
|
'history.loadMore': 'Carregar mais',
|
|
2189
2288
|
'history.filter': 'Texto ou hash',
|
|
@@ -2386,6 +2485,15 @@ window.__ModuleLoader__.load({
|
|
|
2386
2485
|
'menu.sendFilePathToChat': 'Invia il percorso del file alla conversazione',
|
|
2387
2486
|
'send.commitText': 'commit git {hash} "{subject}" (repository {repo})',
|
|
2388
2487
|
'send.branchText': 'branch git {name} (repository {repo})',
|
|
2488
|
+
'menu.sendDiffLinesToChat': 'Invia le righe selezionate alla conversazione',
|
|
2489
|
+
'menu.sendDiffSnippetToChat': 'Invia il frammento selezionato alla conversazione',
|
|
2490
|
+
'menu.copyDiffRef': 'Copia il riferimento alla selezione',
|
|
2491
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
2492
|
+
'send.diffLinesWorktree': '{lines} (albero di lavoro, {state})',
|
|
2493
|
+
'send.diffStaged': 'in stage',
|
|
2494
|
+
'send.diffUnstaged': 'senza commit',
|
|
2495
|
+
'send.diffNewLines': 'righe {lines}',
|
|
2496
|
+
'send.diffOldLines': 'righe {lines} (numerazione precedente)',
|
|
2389
2497
|
'history.empty': 'Questo repository non ha ancora commit',
|
|
2390
2498
|
'history.loadMore': 'Carica altro',
|
|
2391
2499
|
'history.filter': 'Testo o hash',
|
|
@@ -2588,6 +2696,15 @@ window.__ModuleLoader__.load({
|
|
|
2588
2696
|
'menu.sendFilePathToChat': 'Bestandspad naar het gesprek verzenden',
|
|
2589
2697
|
'send.commitText': 'git-commit {hash} "{subject}" (repository {repo})',
|
|
2590
2698
|
'send.branchText': 'git-branch {name} (repository {repo})',
|
|
2699
|
+
'menu.sendDiffLinesToChat': 'Geselecteerde regels naar het gesprek verzenden',
|
|
2700
|
+
'menu.sendDiffSnippetToChat': 'Geselecteerd fragment naar het gesprek verzenden',
|
|
2701
|
+
'menu.copyDiffRef': 'Selectiereferentie kopiëren',
|
|
2702
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
2703
|
+
'send.diffLinesWorktree': '{lines} (werkboom, {state})',
|
|
2704
|
+
'send.diffStaged': 'staged',
|
|
2705
|
+
'send.diffUnstaged': 'niet vastgelegd',
|
|
2706
|
+
'send.diffNewLines': 'regels {lines}',
|
|
2707
|
+
'send.diffOldLines': 'regels {lines} (oude nummering)',
|
|
2591
2708
|
'history.empty': 'Deze repository heeft nog geen commits',
|
|
2592
2709
|
'history.loadMore': 'Meer laden',
|
|
2593
2710
|
'history.filter': 'Tekst of hash',
|
|
@@ -2790,6 +2907,15 @@ window.__ModuleLoader__.load({
|
|
|
2790
2907
|
'menu.sendFilePathToChat': 'Wyślij ścieżkę pliku do rozmowy',
|
|
2791
2908
|
'send.commitText': 'commit git {hash} „{subject}" (repozytorium {repo})',
|
|
2792
2909
|
'send.branchText': 'gałąź git {name} (repozytorium {repo})',
|
|
2910
|
+
'menu.sendDiffLinesToChat': 'Wyślij zaznaczone wiersze do rozmowy',
|
|
2911
|
+
'menu.sendDiffSnippetToChat': 'Wyślij zaznaczony fragment do rozmowy',
|
|
2912
|
+
'menu.copyDiffRef': 'Skopiuj odwołanie do zaznaczenia',
|
|
2913
|
+
'send.diffLinesCommit': '{lines} (commit {hash} „{subject}" @ {branch})',
|
|
2914
|
+
'send.diffLinesWorktree': '{lines} (drzewo robocze, {state})',
|
|
2915
|
+
'send.diffStaged': 'przygotowane',
|
|
2916
|
+
'send.diffUnstaged': 'niezacommitowane',
|
|
2917
|
+
'send.diffNewLines': 'wiersze {lines}',
|
|
2918
|
+
'send.diffOldLines': 'wiersze {lines} (stara numeracja)',
|
|
2793
2919
|
'history.empty': 'To repozytorium nie ma jeszcze commitów',
|
|
2794
2920
|
'history.loadMore': 'Wczytaj więcej',
|
|
2795
2921
|
'history.filter': 'Tekst lub hash',
|
|
@@ -2992,6 +3118,15 @@ window.__ModuleLoader__.load({
|
|
|
2992
3118
|
'menu.sendFilePathToChat': 'Skicka filsökvägen till samtalet',
|
|
2993
3119
|
'send.commitText': 'git-commit {hash} ”{subject}” (arkiv {repo})',
|
|
2994
3120
|
'send.branchText': 'git-gren {name} (arkiv {repo})',
|
|
3121
|
+
'menu.sendDiffLinesToChat': 'Skicka markerade rader till samtalet',
|
|
3122
|
+
'menu.sendDiffSnippetToChat': 'Skicka markerat utdrag till samtalet',
|
|
3123
|
+
'menu.copyDiffRef': 'Kopiera markeringens referens',
|
|
3124
|
+
'send.diffLinesCommit': '{lines} (incheckning {hash} ”{subject}” @ {branch})',
|
|
3125
|
+
'send.diffLinesWorktree': '{lines} (arbetskopia, {state})',
|
|
3126
|
+
'send.diffStaged': 'staged',
|
|
3127
|
+
'send.diffUnstaged': 'ej incheckad',
|
|
3128
|
+
'send.diffNewLines': 'rader {lines}',
|
|
3129
|
+
'send.diffOldLines': 'rader {lines} (gammal numrering)',
|
|
2995
3130
|
'history.empty': 'Det här arkivet har inga commits ännu',
|
|
2996
3131
|
'history.loadMore': 'Ladda mer',
|
|
2997
3132
|
'history.filter': 'Text eller hash',
|
|
@@ -3194,6 +3329,15 @@ window.__ModuleLoader__.load({
|
|
|
3194
3329
|
'menu.sendFilePathToChat': 'Dosya yolunu sohbete gönder',
|
|
3195
3330
|
'send.commitText': 'git commit {hash} "{subject}" (depo {repo})',
|
|
3196
3331
|
'send.branchText': 'git dalı {name} (depo {repo})',
|
|
3332
|
+
'menu.sendDiffLinesToChat': 'Seçili satırları sohbete gönder',
|
|
3333
|
+
'menu.sendDiffSnippetToChat': 'Seçili parçayı sohbete gönder',
|
|
3334
|
+
'menu.copyDiffRef': 'Seçim referansını kopyala',
|
|
3335
|
+
'send.diffLinesCommit': '{lines} (işleme {hash} "{subject}" @ {branch})',
|
|
3336
|
+
'send.diffLinesWorktree': '{lines} (çalışma ağacı, {state})',
|
|
3337
|
+
'send.diffStaged': 'hazırlanmış',
|
|
3338
|
+
'send.diffUnstaged': 'işlenmemiş',
|
|
3339
|
+
'send.diffNewLines': 'satırlar {lines}',
|
|
3340
|
+
'send.diffOldLines': 'satırlar {lines} (eski numaralandırma)',
|
|
3197
3341
|
'history.empty': 'Bu depoda henüz commit yok',
|
|
3198
3342
|
'history.loadMore': 'Daha fazla yükle',
|
|
3199
3343
|
'history.filter': 'Metin veya hash',
|
|
@@ -3396,6 +3540,15 @@ window.__ModuleLoader__.load({
|
|
|
3396
3540
|
'menu.sendFilePathToChat': 'Kirim jalur berkas ke obrolan',
|
|
3397
3541
|
'send.commitText': 'commit git {hash} "{subject}" (repositori {repo})',
|
|
3398
3542
|
'send.branchText': 'cabang git {name} (repositori {repo})',
|
|
3543
|
+
'menu.sendDiffLinesToChat': 'Kirim baris terpilih ke obrolan',
|
|
3544
|
+
'menu.sendDiffSnippetToChat': 'Kirim cuplikan terpilih ke obrolan',
|
|
3545
|
+
'menu.copyDiffRef': 'Salin referensi pilihan',
|
|
3546
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
3547
|
+
'send.diffLinesWorktree': '{lines} (pohon kerja, {state})',
|
|
3548
|
+
'send.diffStaged': 'di-stage',
|
|
3549
|
+
'send.diffUnstaged': 'belum di-commit',
|
|
3550
|
+
'send.diffNewLines': 'baris {lines}',
|
|
3551
|
+
'send.diffOldLines': 'baris {lines} (penomoran lama)',
|
|
3399
3552
|
'history.empty': 'Repositori ini belum punya commit',
|
|
3400
3553
|
'history.loadMore': 'Muat lebih banyak',
|
|
3401
3554
|
'history.filter': 'Teks atau hash',
|
|
@@ -3598,6 +3751,15 @@ window.__ModuleLoader__.load({
|
|
|
3598
3751
|
'menu.sendFilePathToChat': 'Gửi đường dẫn tệp vào hội thoại',
|
|
3599
3752
|
'send.commitText': 'git commit {hash} "{subject}" (kho {repo})',
|
|
3600
3753
|
'send.branchText': 'nhánh git {name} (kho {repo})',
|
|
3754
|
+
'menu.sendDiffLinesToChat': 'Gửi các dòng đã chọn vào hội thoại',
|
|
3755
|
+
'menu.sendDiffSnippetToChat': 'Gửi đoạn đã chọn vào hội thoại',
|
|
3756
|
+
'menu.copyDiffRef': 'Sao chép tham chiếu vùng chọn',
|
|
3757
|
+
'send.diffLinesCommit': '{lines} (commit {hash} "{subject}" @ {branch})',
|
|
3758
|
+
'send.diffLinesWorktree': '{lines} (cây làm việc, {state})',
|
|
3759
|
+
'send.diffStaged': 'đã staged',
|
|
3760
|
+
'send.diffUnstaged': 'chưa commit',
|
|
3761
|
+
'send.diffNewLines': 'dòng {lines}',
|
|
3762
|
+
'send.diffOldLines': 'dòng {lines} (đánh số cũ)',
|
|
3601
3763
|
'history.empty': 'Kho này chưa có commit nào',
|
|
3602
3764
|
'history.loadMore': 'Tải thêm',
|
|
3603
3765
|
'history.filter': 'Văn bản hoặc hash',
|
|
@@ -3800,6 +3962,15 @@ window.__ModuleLoader__.load({
|
|
|
3800
3962
|
'menu.sendFilePathToChat': 'إرسال مسار الملف إلى المحادثة',
|
|
3801
3963
|
'send.commitText': 'التزام git {hash} "{subject}" (المستودع {repo})',
|
|
3802
3964
|
'send.branchText': 'فرع git {name} (المستودع {repo})',
|
|
3965
|
+
'menu.sendDiffLinesToChat': 'إرسال الأسطر المحددة إلى المحادثة',
|
|
3966
|
+
'menu.sendDiffSnippetToChat': 'إرسال المقتطف المحدد إلى المحادثة',
|
|
3967
|
+
'menu.copyDiffRef': 'نسخ مرجع التحديد',
|
|
3968
|
+
'send.diffLinesCommit': '{lines} (التزام {hash} "{subject}" @ {branch})',
|
|
3969
|
+
'send.diffLinesWorktree': '{lines} (شجرة العمل، {state})',
|
|
3970
|
+
'send.diffStaged': 'مُرحَّل',
|
|
3971
|
+
'send.diffUnstaged': 'غير مُلتزم',
|
|
3972
|
+
'send.diffNewLines': 'الأسطر {lines}',
|
|
3973
|
+
'send.diffOldLines': 'الأسطر {lines} (ترقيم قديم)',
|
|
3803
3974
|
'history.empty': 'لا توجد التزامات في هذا المستودع بعد',
|
|
3804
3975
|
'history.loadMore': 'تحميل المزيد',
|
|
3805
3976
|
'history.filter': 'نص أو تجزئة',
|
|
@@ -4002,6 +4173,15 @@ window.__ModuleLoader__.load({
|
|
|
4002
4173
|
'menu.sendFilePathToChat': 'फ़ाइल पथ बातचीत में भेजें',
|
|
4003
4174
|
'send.commitText': 'git कमिट {hash} "{subject}" (रिपॉज़िटरी {repo})',
|
|
4004
4175
|
'send.branchText': 'git ब्रांच {name} (रिपॉज़िटरी {repo})',
|
|
4176
|
+
'menu.sendDiffLinesToChat': 'चयनित पंक्तियाँ बातचीत में भेजें',
|
|
4177
|
+
'menu.sendDiffSnippetToChat': 'चयनित अंश बातचीत में भेजें',
|
|
4178
|
+
'menu.copyDiffRef': 'चयन का संदर्भ कॉपी करें',
|
|
4179
|
+
'send.diffLinesCommit': '{lines} (कमिट {hash} "{subject}" @ {branch})',
|
|
4180
|
+
'send.diffLinesWorktree': '{lines} (वर्कट्री, {state})',
|
|
4181
|
+
'send.diffStaged': 'स्टेज्ड',
|
|
4182
|
+
'send.diffUnstaged': 'अनकमिटेड',
|
|
4183
|
+
'send.diffNewLines': 'पंक्तियाँ {lines}',
|
|
4184
|
+
'send.diffOldLines': 'पंक्तियाँ {lines} (पुरानी क्रमांक)',
|
|
4005
4185
|
'history.empty': 'इस रिपॉज़िटरी में अभी कोई कमिट नहीं है',
|
|
4006
4186
|
'history.loadMore': 'और लोड करें',
|
|
4007
4187
|
'history.filter': 'टेक्स्ट या हैश',
|
|
@@ -4204,6 +4384,15 @@ window.__ModuleLoader__.load({
|
|
|
4204
4384
|
'menu.sendFilePathToChat': 'ส่งพาธไฟล์ไปยังบทสนทนา',
|
|
4205
4385
|
'send.commitText': 'git คอมมิต {hash} "{subject}" (รีโพซิทอรี {repo})',
|
|
4206
4386
|
'send.branchText': 'git บรานช์ {name} (รีโพซิทอรี {repo})',
|
|
4387
|
+
'menu.sendDiffLinesToChat': 'ส่งบรรทัดที่เลือกไปยังบทสนทนา',
|
|
4388
|
+
'menu.sendDiffSnippetToChat': 'ส่งข้อความที่เลือกไปยังบทสนทนา',
|
|
4389
|
+
'menu.copyDiffRef': 'คัดลอกการอ้างอิงที่เลือก',
|
|
4390
|
+
'send.diffLinesCommit': '{lines} (คอมมิต {hash} "{subject}" @ {branch})',
|
|
4391
|
+
'send.diffLinesWorktree': '{lines} (แผนผังงาน, {state})',
|
|
4392
|
+
'send.diffStaged': 'สเตจแล้ว',
|
|
4393
|
+
'send.diffUnstaged': 'ยังไม่คอมมิต',
|
|
4394
|
+
'send.diffNewLines': 'บรรทัด {lines}',
|
|
4395
|
+
'send.diffOldLines': 'บรรทัด {lines} (เลขบรรทัดเดิม)',
|
|
4207
4396
|
'history.empty': 'รีโพซิทอรีนี้ยังไม่มีคอมมิต',
|
|
4208
4397
|
'history.loadMore': 'โหลดเพิ่ม',
|
|
4209
4398
|
'history.filter': 'ข้อความหรือแฮช',
|
|
@@ -5495,20 +5684,47 @@ window.__ModuleLoader__.load({
|
|
|
5495
5684
|
} catch (error) { void error; return null }
|
|
5496
5685
|
}
|
|
5497
5686
|
|
|
5687
|
+
/* The composer carries TWO text projections (ui-conversation projection.ts):
|
|
5688
|
+
detectText feeds TokenSpan coordinates — every chip counts as ONE U+FFFC
|
|
5689
|
+
there — while the published InputState.draft is the clipboard projection,
|
|
5690
|
+
where a chip expands to its full `@path`. The insert verbs take DETECT
|
|
5691
|
+
spans, and selectSpan rejects `end > detectLength` outright (span-map.ts),
|
|
5692
|
+
so a tail span built from draft.length is correct only while the draft has
|
|
5693
|
+
NO chips. With one in the draft it points past the document end and the
|
|
5694
|
+
edit silently fails — exactly the "chip landed, the line-range sentence
|
|
5695
|
+
never did" real-machine report against v0.13.5 (and any plain-text send
|
|
5696
|
+
into a draft that already holds a chip). Convert instead: subtract every
|
|
5697
|
+
occurrence's (clipboard length - 1). */
|
|
5698
|
+
function detectTailSpanOf(state) {
|
|
5699
|
+
if (state === undefined || state === null || typeof state.draft !== 'string' || typeof state.draftRev !== 'number') return null
|
|
5700
|
+
let at = state.draft.length
|
|
5701
|
+
const occurrences = Array.isArray(state.occurrences) ? state.occurrences : []
|
|
5702
|
+
for (const occ of occurrences) {
|
|
5703
|
+
if (occ === null || typeof occ !== 'object') continue
|
|
5704
|
+
const width = typeof occ.length === 'number' && occ.length >= 0
|
|
5705
|
+
? occ.length
|
|
5706
|
+
: (typeof occ.clipboardText === 'string' ? occ.clipboardText.length : 1)
|
|
5707
|
+
at -= Math.max(0, width - 1)
|
|
5708
|
+
}
|
|
5709
|
+
if (at < 0) return null
|
|
5710
|
+
return { start: at, end: at, draftRev: state.draftRev }
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5498
5713
|
/* apply(actx, input, span) → boolean. The span is an EMPTY span at the draft
|
|
5499
|
-
tail in
|
|
5500
|
-
|
|
5501
|
-
re-read once and retry (the brief's advice:
|
|
5502
|
-
busy phases (adjudicating/submitting) also
|
|
5503
|
-
same clipboard fallback, which is the
|
|
5714
|
+
tail in DETECT coordinates (the coordinates the CAS and selectSpan
|
|
5715
|
+
compare — see detectTailSpanOf). A false answer is the user typing
|
|
5716
|
+
between our read and write — re-read once and retry (the brief's advice:
|
|
5717
|
+
one retry, no spinning). The busy phases (adjudicating/submitting) also
|
|
5718
|
+
answer false; that lands on the same clipboard fallback, which is the
|
|
5719
|
+
honest outcome. */
|
|
5504
5720
|
function insertIntoComposer(ctx, sessionId, apply) {
|
|
5505
5721
|
const target = composerTargetOf(ctx, sessionId)
|
|
5506
5722
|
if (target === null) return false
|
|
5507
5723
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5508
5724
|
let state
|
|
5509
5725
|
try { state = target.input.state.getSnapshot() } catch (error) { void error; return false }
|
|
5510
|
-
|
|
5511
|
-
|
|
5726
|
+
const span = detectTailSpanOf(state)
|
|
5727
|
+
if (span === null) return false
|
|
5512
5728
|
let applied = false
|
|
5513
5729
|
try { applied = apply(target.actx, target.input, span) === true } catch (error) { void error; applied = false }
|
|
5514
5730
|
if (applied === true) {
|
|
@@ -5576,7 +5792,11 @@ window.__ModuleLoader__.load({
|
|
|
5576
5792
|
} else if (line.indexOf('@@') === 0) {
|
|
5577
5793
|
kind = 'hunk'
|
|
5578
5794
|
const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line)
|
|
5579
|
-
|
|
5795
|
+
// The @@ header names the FIRST content line (git semantics), but the
|
|
5796
|
+
// counters below are PRE-INCREMENTED before they land in a row — so
|
|
5797
|
+
// seed one less. Off by one otherwise: v0.13.3's hand-drawn gutter
|
|
5798
|
+
// numbered every line one too high (surfaced by the selection core).
|
|
5799
|
+
if (match !== null) { oldLine = Number.parseInt(match[1], 10) - 1; newLine = Number.parseInt(match[2], 10) - 1 }
|
|
5580
5800
|
} else if (line.indexOf('+') === 0) { kind = 'add'; newLine += 1 }
|
|
5581
5801
|
else if (line.indexOf('-') === 0) { kind = 'del'; oldLine += 1 }
|
|
5582
5802
|
else if (line.indexOf('\\') === 0) { kind = 'meta' }
|
|
@@ -5590,6 +5810,250 @@ window.__ModuleLoader__.load({
|
|
|
5590
5810
|
return out
|
|
5591
5811
|
}
|
|
5592
5812
|
|
|
5813
|
+
/* ---- diff selection core
|
|
5814
|
+
Pure line-number plumbing for the diff-pane context menu (v0.13.5).
|
|
5815
|
+
The host DiffBlock draws rows WITHOUT line-number attributes — a row is
|
|
5816
|
+
`<div class={line + kindClass}>{row.text}</div>` (DiffBlock.tsx:204) and
|
|
5817
|
+
the `+`/`-`/two-space prefixes live in CSS `::before` content — so the
|
|
5818
|
+
menu reconstructs numbers by replaying the ROW KINDS in document order
|
|
5819
|
+
from the patch's first hunk header. The hand-drawn rows already carry
|
|
5820
|
+
parsed numbers (diffLines above). Kept DOM-free between the markers so
|
|
5821
|
+
tests/smoke.mjs can slice the block and drive the real implementation. */
|
|
5822
|
+
|
|
5823
|
+
const DIFF_SNIPPET_MAX = 600
|
|
5824
|
+
|
|
5825
|
+
/* First hunk header of a single-file patch → the absolute numbers the
|
|
5826
|
+
replay starts from (`@@ -a,b +c,d @@`: a/c are the sides' first lines).
|
|
5827
|
+
A patch without any header (binary, mode-only) has no coordinates. */
|
|
5828
|
+
function hunkStartOf(patch) {
|
|
5829
|
+
const headers = hunkHeadersOf(patch)
|
|
5830
|
+
return headers === null ? null : headers[0]
|
|
5831
|
+
}
|
|
5832
|
+
|
|
5833
|
+
/* Every hunk header in patch order: a multi-hunk diff restarts BOTH
|
|
5834
|
+
counters at each `@@` line, so the replay needs one seed per hunk. */
|
|
5835
|
+
function hunkHeadersOf(patch) {
|
|
5836
|
+
const text = typeof patch === 'string' ? patch : ''
|
|
5837
|
+
if (text.indexOf('@@') < 0) return null
|
|
5838
|
+
const headers = []
|
|
5839
|
+
const pattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/gm
|
|
5840
|
+
let match = pattern.exec(text)
|
|
5841
|
+
while (match !== null) {
|
|
5842
|
+
headers.push({ oldLine: Number.parseInt(match[1], 10), newLine: Number.parseInt(match[2], 10) })
|
|
5843
|
+
match = pattern.exec(text)
|
|
5844
|
+
}
|
|
5845
|
+
return headers.length === 0 ? null : headers
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5848
|
+
/* getComputedStyle(el, '::before').content → the row kind DiffBlock
|
|
5849
|
+
paints: `- ` del, `+ ` add, two spaces context, nothing for path/gap
|
|
5850
|
+
chrome. Browsers quote the value ("+ "); strip quotes before comparing. */
|
|
5851
|
+
function beforeContentKind(content) {
|
|
5852
|
+
if (typeof content !== 'string') return null
|
|
5853
|
+
const value = content.length >= 2 && content.charAt(0) === '"' && content.charAt(content.length - 1) === '"'
|
|
5854
|
+
? content.slice(1, -1)
|
|
5855
|
+
: content
|
|
5856
|
+
if (value === '- ') return 'del'
|
|
5857
|
+
if (value === '+ ') return 'add'
|
|
5858
|
+
if (value === ' ') return 'ctx'
|
|
5859
|
+
return null
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5862
|
+
/* Replay kinds[0..to] (document order), collecting the span covered by
|
|
5863
|
+
kinds[from..to]. Each hunk restarts BOTH counters from its own header:
|
|
5864
|
+
a chrome row (path/gap, kind null) marks the boundary, and the next
|
|
5865
|
+
content row consumes the NEXT header in patch order — the path row at
|
|
5866
|
+
the top then the gap rows between hunks. New-side numbers win; a purely
|
|
5867
|
+
deleted span reports the OLD numbers with old: true — deleted rows
|
|
5868
|
+
exist on that side only. */
|
|
5869
|
+
function lineSpanFromKinds(kinds, from, to, headers) {
|
|
5870
|
+
if (Array.isArray(kinds) === false || Array.isArray(headers) === false || headers.length === 0) return null
|
|
5871
|
+
if (!(from >= 0 && to >= from && to < kinds.length)) return null
|
|
5872
|
+
let headerIndex = 0
|
|
5873
|
+
let oldLine = 0
|
|
5874
|
+
let newLine = 0
|
|
5875
|
+
let started = false
|
|
5876
|
+
let newFrom = null
|
|
5877
|
+
let newTo = null
|
|
5878
|
+
let oldFrom = null
|
|
5879
|
+
let oldTo = null
|
|
5880
|
+
for (let index = 0; index <= to; index += 1) {
|
|
5881
|
+
const kind = kinds[index]
|
|
5882
|
+
if (kind !== 'del' && kind !== 'add' && kind !== 'ctx') {
|
|
5883
|
+
started = false // chrome row: the next content row starts a new hunk
|
|
5884
|
+
continue
|
|
5885
|
+
}
|
|
5886
|
+
if (started === false) {
|
|
5887
|
+
if (headerIndex >= headers.length) break // more hunks than headers: stop, don't guess
|
|
5888
|
+
const header = headers[headerIndex]
|
|
5889
|
+
headerIndex += 1
|
|
5890
|
+
oldLine = header.oldLine
|
|
5891
|
+
newLine = header.newLine
|
|
5892
|
+
started = true
|
|
5893
|
+
}
|
|
5894
|
+
let oldHit = null
|
|
5895
|
+
let newHit = null
|
|
5896
|
+
if (kind === 'del') { oldHit = oldLine; oldLine += 1 }
|
|
5897
|
+
else if (kind === 'add') { newHit = newLine; newLine += 1 }
|
|
5898
|
+
else { oldHit = oldLine; newHit = newLine; oldLine += 1; newLine += 1 }
|
|
5899
|
+
if (index >= from) {
|
|
5900
|
+
if (oldHit !== null) { oldFrom = oldFrom === null ? oldHit : oldFrom; oldTo = oldHit }
|
|
5901
|
+
if (newHit !== null) { newFrom = newFrom === null ? newHit : newFrom; newTo = newHit }
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
if (newFrom !== null) return { start: newFrom, end: newTo === null ? newFrom : newTo, old: false }
|
|
5905
|
+
if (oldFrom !== null) return { start: oldFrom, end: oldTo === null ? oldFrom : oldTo, old: true }
|
|
5906
|
+
return null
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
/* The hand-drawn rows already carry parsed numbers: reduce the selected
|
|
5910
|
+
row range exactly like lineSpanFromKinds reduces replayed kinds. */
|
|
5911
|
+
function rowSpanOfRows(rows, from, to) {
|
|
5912
|
+
if (Array.isArray(rows) === false) return null
|
|
5913
|
+
if (!(from >= 0 && to >= from && to < rows.length)) return null
|
|
5914
|
+
let newFrom = null
|
|
5915
|
+
let newTo = null
|
|
5916
|
+
let oldFrom = null
|
|
5917
|
+
let oldTo = null
|
|
5918
|
+
for (let index = from; index <= to; index += 1) {
|
|
5919
|
+
const row = rows[index]
|
|
5920
|
+
if (row === undefined || row === null) continue
|
|
5921
|
+
const newHit = typeof row.newLine === 'number' ? row.newLine : null
|
|
5922
|
+
const oldHit = typeof row.oldLine === 'number' ? row.oldLine : null
|
|
5923
|
+
if (newHit !== null) { newFrom = newFrom === null ? newHit : newFrom; newTo = newHit }
|
|
5924
|
+
if (oldHit !== null) { oldFrom = oldFrom === null ? oldHit : oldFrom; oldTo = oldHit }
|
|
5925
|
+
}
|
|
5926
|
+
if (newFrom !== null) return { start: newFrom, end: newTo === null ? newFrom : newTo, old: false }
|
|
5927
|
+
if (oldFrom !== null) return { start: oldFrom, end: oldTo === null ? oldFrom : oldTo, old: true }
|
|
5928
|
+
return null
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
/* '7' or '7–9' — the {lines} value the payload templates receive. */
|
|
5932
|
+
function linesLabelOf(start, end) {
|
|
5933
|
+
return start === end ? String(start) : String(start) + '–' + String(end)
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
/* The localized line-span fragment the payload templates embed: new-side
|
|
5937
|
+
numbers read plain, old-side numbers say so instead of pretending. */
|
|
5938
|
+
function lineSpanLabelOf(t, span) {
|
|
5939
|
+
const lines = linesLabelOf(span.start, span.end)
|
|
5940
|
+
return fill(t(span.old === true ? 'send.diffOldLines' : 'send.diffNewLines'), { lines: lines })
|
|
5941
|
+
}
|
|
5942
|
+
|
|
5943
|
+
/* The self-describing line reference text for one resolved span.
|
|
5944
|
+
Commit context carries hash + subject (truncated like the commit send)
|
|
5945
|
+
and the HEAD branch; the worktree pane says staged or uncommitted. */
|
|
5946
|
+
function diffSpanTextOf(t, context, span, branchName) {
|
|
5947
|
+
const lines = lineSpanLabelOf(t, span)
|
|
5948
|
+
if (context === null || context === undefined || context.kind !== 'commit') {
|
|
5949
|
+
return fill(t('send.diffLinesWorktree'), {
|
|
5950
|
+
lines: lines,
|
|
5951
|
+
state: t(context !== null && context !== undefined && context.staged === true ? 'send.diffStaged' : 'send.diffUnstaged'),
|
|
5952
|
+
})
|
|
5953
|
+
}
|
|
5954
|
+
const subject = typeof context.subject === 'string' ? context.subject.trim().slice(0, 60) : ''
|
|
5955
|
+
return fill(t('send.diffLinesCommit'), {
|
|
5956
|
+
lines: lines,
|
|
5957
|
+
hash: typeof context.hash === 'string' && context.hash !== '' ? context.hash.slice(0, 7) : '—',
|
|
5958
|
+
subject: subject === '' ? '—' : subject,
|
|
5959
|
+
branch: typeof branchName === 'string' && branchName !== '' ? branchName : '—',
|
|
5960
|
+
})
|
|
5961
|
+
}
|
|
5962
|
+
|
|
5963
|
+
/* ---- end diff selection core */
|
|
5964
|
+
|
|
5965
|
+
/* ---------- DOM side of the diff-pane context menu (browser only) ----------
|
|
5966
|
+
SSR never calls these: they run inside the pane's onContextMenu, after
|
|
5967
|
+
the selection has been captured. The row element under a selection
|
|
5968
|
+
endpoint is either a hand-drawn `.dig-diff-line`, or a DiffBlock row —
|
|
5969
|
+
a div child of the body whose OWN parent carries the `data-diff`
|
|
5970
|
+
marker. `data-diff` is a component attribute (DiffBlock.tsx:199), not a
|
|
5971
|
+
hashed CSS-module class, so the lookup does not bet on build-time class
|
|
5972
|
+
renaming; the row KIND comes from the `::before` content the kinds are
|
|
5973
|
+
painted with, for the same reason. */
|
|
5974
|
+
|
|
5975
|
+
function savedSelectionOf() {
|
|
5976
|
+
try {
|
|
5977
|
+
const selection = window.getSelection()
|
|
5978
|
+
if (selection === null || selection.rangeCount === 0) return null
|
|
5979
|
+
const text = String(selection)
|
|
5980
|
+
if (text === '') return null
|
|
5981
|
+
return { text: text, range: selection.getRangeAt(0).cloneRange() }
|
|
5982
|
+
} catch (error) { void error; return null }
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function selectionRowOf(node) {
|
|
5986
|
+
let element = node === null || node === undefined ? null
|
|
5987
|
+
: node.nodeType === 3 ? (node.parentElement === null ? null : node.parentElement)
|
|
5988
|
+
: node.nodeType === 1 ? node
|
|
5989
|
+
: null
|
|
5990
|
+
for (let hop = 0; element !== null && element !== undefined && hop < 8; hop += 1) {
|
|
5991
|
+
if (element.classList !== undefined && element.classList.contains('dig-diff-line') === true) {
|
|
5992
|
+
return { kind: 'drawn', row: element, body: element.parentElement }
|
|
5993
|
+
}
|
|
5994
|
+
const parent = element.parentElement
|
|
5995
|
+
if (parent !== null && parent.getAttribute !== undefined && parent.hasAttribute('data-diff') === false) {
|
|
5996
|
+
const root = parent.parentElement
|
|
5997
|
+
if (root !== null && root.getAttribute !== undefined && root.hasAttribute('data-diff') === true) {
|
|
5998
|
+
return { kind: 'host', row: element, body: parent }
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
element = parent
|
|
6002
|
+
}
|
|
6003
|
+
return null
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
/* The rendered rows of a DiffBlock body, with the kind each one paints.
|
|
6007
|
+
The (never-rendered at this panel's maxLines) fold toggle is a button,
|
|
6008
|
+
not a div — skipped so it cannot shift the row indexes. */
|
|
6009
|
+
function hostRowKinds(body) {
|
|
6010
|
+
const children = body === null || body === undefined ? [] : body.children
|
|
6011
|
+
const rows = []
|
|
6012
|
+
const kinds = []
|
|
6013
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
6014
|
+
const child = children[index]
|
|
6015
|
+
if (child === null || child === undefined || child.tagName === 'BUTTON') continue
|
|
6016
|
+
rows.push(child)
|
|
6017
|
+
let kind = null
|
|
6018
|
+
try { kind = beforeContentKind(window.getComputedStyle(child, '::before').content) } catch (error) { void error; kind = null }
|
|
6019
|
+
kinds.push(kind)
|
|
6020
|
+
}
|
|
6021
|
+
return { rows: rows, kinds: kinds }
|
|
6022
|
+
}
|
|
6023
|
+
|
|
6024
|
+
/* { kind: 'lines', span } for a selection inside ONE diff body, or
|
|
6025
|
+
{ kind: 'snippet' } when the rows are there but the numbers cannot be
|
|
6026
|
+
reconstructed. null (rows missing entirely) means "not a diff
|
|
6027
|
+
selection": the menu keeps its old behaviour. */
|
|
6028
|
+
function resolveDiffSelection(patch, saved) {
|
|
6029
|
+
if (saved === null || saved === undefined || saved.text === '' || saved.range === null || saved.range === undefined) return null
|
|
6030
|
+
const from = selectionRowOf(saved.range.startContainer)
|
|
6031
|
+
const to = selectionRowOf(saved.range.endContainer)
|
|
6032
|
+
if (from === null || to === null) return null
|
|
6033
|
+
if (from.kind !== to.kind || from.body !== to.body) return { kind: 'snippet', text: saved.text }
|
|
6034
|
+
let span = null
|
|
6035
|
+
if (from.kind === 'drawn') {
|
|
6036
|
+
const rows = diffLines(patch)
|
|
6037
|
+
const all = from.body === null || from.body === undefined ? [] : Array.prototype.slice.call(from.body.querySelectorAll('.dig-diff-line'))
|
|
6038
|
+
const fromIndex = all.indexOf(from.row)
|
|
6039
|
+
const toIndex = all.indexOf(to.row)
|
|
6040
|
+
if (fromIndex >= 0 && toIndex >= 0) span = rowSpanOfRows(rows, Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex))
|
|
6041
|
+
} else {
|
|
6042
|
+
const headers = hunkHeadersOf(patch)
|
|
6043
|
+
if (headers !== null) {
|
|
6044
|
+
const collected = hostRowKinds(from.body)
|
|
6045
|
+
const fromIndex = collected.rows.indexOf(from.row)
|
|
6046
|
+
const toIndex = collected.rows.indexOf(to.row)
|
|
6047
|
+
if (fromIndex >= 0 && toIndex >= 0) {
|
|
6048
|
+
span = lineSpanFromKinds(collected.kinds, Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex), headers)
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
if (span === null) return { kind: 'snippet', text: saved.text }
|
|
6053
|
+
return { kind: 'lines', span: span, text: saved.text }
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
|
|
5593
6057
|
/* DiffBlock's own 16-line cap would REPLACE scrolling with a fold button;
|
|
5594
6058
|
this panel keeps the old contract (render everything, scroll inside the
|
|
5595
6059
|
.dig-diff-pane max-height), so the cap is pushed out of reach and the
|
|
@@ -7157,6 +7621,11 @@ window.__ModuleLoader__.load({
|
|
|
7157
7621
|
const [selectedPath, setSelectedPath] = useState(null)
|
|
7158
7622
|
const [patch, setPatch] = useState('')
|
|
7159
7623
|
const [patchLoading, setPatchLoading] = useState(false)
|
|
7624
|
+
/* What the open diff pane shows (v0.13.5): the pane is shared by the
|
|
7625
|
+
commit detail (hash + subject) and the working tree (staged or not),
|
|
7626
|
+
and the selection menu's payload must say which. Cleared everywhere
|
|
7627
|
+
the patch is cleared. */
|
|
7628
|
+
const [diffContext, setDiffContext] = useState(null)
|
|
7160
7629
|
const [menu, setMenu] = useState(null)
|
|
7161
7630
|
const [dialog, setDialog] = useState(null)
|
|
7162
7631
|
const [tick, setTick] = useState(0)
|
|
@@ -7523,6 +7992,7 @@ window.__ModuleLoader__.load({
|
|
|
7523
7992
|
setDetail(null)
|
|
7524
7993
|
setSelectedHash(null)
|
|
7525
7994
|
setPatch('')
|
|
7995
|
+
setDiffContext(null)
|
|
7526
7996
|
setView('history')
|
|
7527
7997
|
setPathFilter('')
|
|
7528
7998
|
}, [sessionId])
|
|
@@ -7544,6 +8014,7 @@ window.__ModuleLoader__.load({
|
|
|
7544
8014
|
setSelectedHash(commit.hash)
|
|
7545
8015
|
setSelectedPath(null)
|
|
7546
8016
|
setPatch('')
|
|
8017
|
+
setDiffContext(null)
|
|
7547
8018
|
setView('detail')
|
|
7548
8019
|
setDetail(null)
|
|
7549
8020
|
try {
|
|
@@ -7620,6 +8091,41 @@ window.__ModuleLoader__.load({
|
|
|
7620
8091
|
and a toast says so. */
|
|
7621
8092
|
const sendToComposer = useCallback((payload) => {
|
|
7622
8093
|
const applied = insertIntoComposer(props.ctx, sessionId, (actx, input, span) => {
|
|
8094
|
+
if (payload.mention !== undefined && payload.text !== undefined) {
|
|
8095
|
+
// Chip + trailing sentence (v0.13.5 diff line refs): the @file chip
|
|
8096
|
+
// first, then the line-range text at the NEW draft tail (the chip
|
|
8097
|
+
// changed both the draft and its rev). The facade appends a
|
|
8098
|
+
// separating space after a chip when none follows, so the text leg
|
|
8099
|
+
// goes in verbatim. If the text leg loses its CAS race twice the
|
|
8100
|
+
// chip still stands — the reference made it, that reads as sent.
|
|
8101
|
+
let chipApplied = false
|
|
8102
|
+
try {
|
|
8103
|
+
chipApplied = input.insertReference({
|
|
8104
|
+
source: 'reference',
|
|
8105
|
+
ref: payload.mention,
|
|
8106
|
+
label: payload.label,
|
|
8107
|
+
appearance: payload.appearance,
|
|
8108
|
+
clipboardText: payload.mention,
|
|
8109
|
+
}, span) === true
|
|
8110
|
+
} catch (error) { void error; chipApplied = false }
|
|
8111
|
+
if (chipApplied === false) return false
|
|
8112
|
+
try {
|
|
8113
|
+
let state = input.state.getSnapshot()
|
|
8114
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
8115
|
+
// The chip changed BOTH projections; the tail must be rebuilt
|
|
8116
|
+
// in detect coordinates (see detectTailSpanOf), or selectSpan
|
|
8117
|
+
// rejects it and the sentence silently vanishes.
|
|
8118
|
+
const tail = detectTailSpanOf(state)
|
|
8119
|
+
if (tail !== null) {
|
|
8120
|
+
let textApplied = false
|
|
8121
|
+
try { textApplied = actx.bail(actx, 'slash/input-insert-text', { text: payload.text, span: tail }) === true } catch (error) { void error; textApplied = false }
|
|
8122
|
+
if (textApplied === true) return true
|
|
8123
|
+
}
|
|
8124
|
+
try { state = input.state.getSnapshot() } catch (error) { void error; return true }
|
|
8125
|
+
}
|
|
8126
|
+
} catch (error) { void error }
|
|
8127
|
+
return true
|
|
8128
|
+
}
|
|
7623
8129
|
if (payload.appearance !== undefined) {
|
|
7624
8130
|
return input.insertReference({
|
|
7625
8131
|
source: 'reference',
|
|
@@ -7717,7 +8223,7 @@ window.__ModuleLoader__.load({
|
|
|
7717
8223
|
|
|
7718
8224
|
const changeMenu = useCallback((event, item, group) => {
|
|
7719
8225
|
const items = [
|
|
7720
|
-
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { void openDiff({ path: item.path, staged: group === 'staged' }) } },
|
|
8226
|
+
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { setDiffContext({ kind: 'worktree', staged: group === 'staged' }); void openDiff({ path: item.path, staged: group === 'staged' }) } },
|
|
7721
8227
|
{ id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(item.path) },
|
|
7722
8228
|
{ id: 'sendPath', icon: 'file', tone: 'secondary', label: t('menu.sendFilePathToChat'), run: () => sendToComposer({ text: item.path, fallback: item.path }) },
|
|
7723
8229
|
null,
|
|
@@ -7735,7 +8241,11 @@ window.__ModuleLoader__.load({
|
|
|
7735
8241
|
staging verbs: a committed file's diff replays against its commit. */
|
|
7736
8242
|
const detailFileMenu = useCallback((event, file) => {
|
|
7737
8243
|
openMenuAt(event, [
|
|
7738
|
-
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => {
|
|
8244
|
+
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => {
|
|
8245
|
+
setSelectedPath(file.path)
|
|
8246
|
+
setDiffContext({ kind: 'commit', hash: detail === null || detail === undefined ? undefined : detail.hash, subject: detail === null || detail === undefined ? '' : detail.subject })
|
|
8247
|
+
void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
|
|
8248
|
+
} },
|
|
7739
8249
|
null,
|
|
7740
8250
|
{ id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(file.path) },
|
|
7741
8251
|
{ id: 'sendPath', icon: 'file', tone: 'secondary', label: t('menu.sendFilePathToChat'), run: () => sendToComposer({ text: file.path, fallback: file.path }) },
|
|
@@ -7744,6 +8254,66 @@ window.__ModuleLoader__.load({
|
|
|
7744
8254
|
])
|
|
7745
8255
|
}, [detail, openDiff, openMenuAt, sendFileReferenceToChat, sendToComposer, t])
|
|
7746
8256
|
|
|
8257
|
+
/* The diff pane's own context menu (v0.13.5): select text in the diff,
|
|
8258
|
+
right-click, send a self-describing line reference to the composer.
|
|
8259
|
+
The window selection is read BEFORE the menu opens — some platforms
|
|
8260
|
+
clear it the moment a context menu shows — and the whole payload is
|
|
8261
|
+
computed right here, so the menu items run from captured values and
|
|
8262
|
+
never depend on a selection that may no longer exist. A selection
|
|
8263
|
+
outside the diff rows keeps the browser's own menu: preventDefault is
|
|
8264
|
+
skipped and no item appears (the old behaviour).
|
|
8265
|
+
|
|
8266
|
+
LAST-RESORT SEMANTICS (v0.13.5 forward compat): the rows under this
|
|
8267
|
+
pane may be HOST-rendered (ui-primitives DiffBlock), and a future DSH
|
|
8268
|
+
is free to add its own context-menu surface to that component. React
|
|
8269
|
+
dispatches synthetic contextmenu handlers in bubble order — innermost
|
|
8270
|
+
first — so an inner handler that claimed the event (opened its menu
|
|
8271
|
+
or called preventDefault) has already set defaultPrevented by the
|
|
8272
|
+
time this container handler runs. That check is the FIRST statement:
|
|
8273
|
+
if the host handled it we return and the host menu stands alone —
|
|
8274
|
+
never two menus at once, never an override. An unusable selection
|
|
8275
|
+
also returns without preventDefault (browser menu preserved). Only a
|
|
8276
|
+
valid diff selection that NOTHING else handled reaches our menu.
|
|
8277
|
+
UPGRADE MEMO (diff-右键向前兼容): DiffBlock today exposes NO menu
|
|
8278
|
+
surface and NO extension prop — DiffBlockProps is exactly
|
|
8279
|
+
{ diffs, labels, maxLines, className } (ui-primitives DiffBlock.tsx);
|
|
8280
|
+
its only stable hook is the `data-diff` marker on the root div, which
|
|
8281
|
+
this pane already keys on. When upstream ships a menu surface (most
|
|
8282
|
+
likely an onRowContextMenu / items prop mounted on the row div or the
|
|
8283
|
+
body container), MIGRATE to that prop — feed it our line-reference
|
|
8284
|
+
items and RETIRE this container-level handler — the absorption
|
|
8285
|
+
pattern dsh-better-workspace uses (host menu surface + host
|
|
8286
|
+
dictionary, own entries appended last), not an event race. Until
|
|
8287
|
+
then this handler stays deliberately last-resort. */
|
|
8288
|
+
const onDiffPaneContextMenu = useCallback((event) => {
|
|
8289
|
+
if (event.defaultPrevented === true) return // an inner (host DiffBlock) handler already owns this event
|
|
8290
|
+
const saved = savedSelectionOf()
|
|
8291
|
+
const resolved = saved === null ? null : resolveDiffSelection(patch, saved)
|
|
8292
|
+
if (saved === null || resolved === null) return
|
|
8293
|
+
event.preventDefault()
|
|
8294
|
+
const path = selectedPath
|
|
8295
|
+
const hasFile = typeof path === 'string' && path !== ''
|
|
8296
|
+
const branchName = branches === null ? '' : branches.branch
|
|
8297
|
+
const mention = hasFile === true ? fileMentionOf(path) : null
|
|
8298
|
+
const snippet = resolved.text.length > DIFF_SNIPPET_MAX ? resolved.text.slice(0, DIFF_SNIPPET_MAX) : resolved.text
|
|
8299
|
+
const text = resolved.kind === 'lines' && hasFile === true
|
|
8300
|
+
? diffSpanTextOf(t, diffContext, resolved.span, branchName)
|
|
8301
|
+
: snippet
|
|
8302
|
+
const plain = (mention === null ? (hasFile === true ? path : '') : mention) + ' ' + text
|
|
8303
|
+
const sendChip = () => {
|
|
8304
|
+
if (mention === null || hasFile === false) { sendToComposer({ text: text, fallback: text }); return }
|
|
8305
|
+
sendToComposer({ mention: mention, label: baseName(path), appearance: 'file', text: text, fallback: plain })
|
|
8306
|
+
}
|
|
8307
|
+
const items = [
|
|
8308
|
+
hasFile === true && resolved.kind === 'lines'
|
|
8309
|
+
? { id: 'sendDiffLines', icon: 'send', tone: 'accent', label: t('menu.sendDiffLinesToChat'), run: sendChip }
|
|
8310
|
+
: { id: 'sendDiffSnippet', icon: 'send', tone: 'accent', label: t(resolved.kind === 'lines' ? 'menu.sendDiffLinesToChat' : 'menu.sendDiffSnippetToChat'), run: sendChip },
|
|
8311
|
+
null,
|
|
8312
|
+
{ id: 'copyDiffRef', icon: 'tag', tone: 'secondary', label: t('menu.copyDiffRef'), run: () => { copyText(plain) } },
|
|
8313
|
+
]
|
|
8314
|
+
openMenuAt(event, items)
|
|
8315
|
+
}, [patch, selectedPath, diffContext, branches, openMenuAt, sendToComposer, t])
|
|
8316
|
+
|
|
7747
8317
|
const submitDialog = useCallback(async (state, value) => {
|
|
7748
8318
|
setDialog(null)
|
|
7749
8319
|
if (state.kind === 'newBranch') { await run('checkout', { branch: value, create: true, startPoint: state.from }); return }
|
|
@@ -7915,16 +8485,17 @@ window.__ModuleLoader__.load({
|
|
|
7915
8485
|
onStageAll: (entries) => { void run('stage', { paths: entries.map((entry) => entry.path) }) },
|
|
7916
8486
|
onUnstageAll: () => { void run('unstage', { paths: (summary === null ? [] : summary.changes.staged).map((entry) => entry.path) }) },
|
|
7917
8487
|
onDiscard: (item, group) => setDialog({ kind: 'discard', item: item, group: group }),
|
|
7918
|
-
onDiff: (item, group) => { void openDiff({ path: item.path, staged: group === 'staged' }) },
|
|
8488
|
+
onDiff: (item, group) => { setDiffContext({ kind: 'worktree', staged: group === 'staged' }); void openDiff({ path: item.path, staged: group === 'staged' }) },
|
|
7919
8489
|
onChangeMenu: changeMenu,
|
|
7920
8490
|
})
|
|
7921
8491
|
|
|
7922
8492
|
const historyPane = view === 'detail'
|
|
7923
8493
|
? E(CommitDetail, {
|
|
7924
8494
|
t: t, detail: detail, selectedPath: selectedPath,
|
|
7925
|
-
onBack: () => { setView('history'); setSelectedPath(null); setPatch('') },
|
|
8495
|
+
onBack: () => { setView('history'); setSelectedPath(null); setPatch(''); setDiffContext(null) },
|
|
7926
8496
|
onSelectFile: (file) => {
|
|
7927
8497
|
setSelectedPath(file.path)
|
|
8498
|
+
setDiffContext({ kind: 'commit', hash: detail === null || detail === undefined ? undefined : detail.hash, subject: detail === null || detail === undefined ? '' : detail.subject })
|
|
7928
8499
|
void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
|
|
7929
8500
|
},
|
|
7930
8501
|
onFileMenu: detailFileMenu,
|
|
@@ -7938,10 +8509,10 @@ window.__ModuleLoader__.load({
|
|
|
7938
8509
|
onLoadMore: () => { void guard(() => loadCommits(commits.length)) },
|
|
7939
8510
|
})
|
|
7940
8511
|
|
|
7941
|
-
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff') },
|
|
8512
|
+
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
7942
8513
|
E('div', { className: 'dig-diff-head' },
|
|
7943
8514
|
E('span', { className: 'dig-mono dig-diff-path' }, selectedPath === null ? '' : selectedPath),
|
|
7944
|
-
E('button', { type: 'button', className: 'dig-icon-btn', onClick: () => { setPatch(''); setSelectedPath(null) } }, E(Icon, { name: 'close', size: 12 }))),
|
|
8515
|
+
E('button', { type: 'button', className: 'dig-icon-btn', onClick: () => { setPatch(''); setSelectedPath(null); setDiffContext(null) } }, E(Icon, { name: 'close', size: 12 }))),
|
|
7945
8516
|
E(DiffBody, { patch: patch, loading: patchLoading, t: t }))
|
|
7946
8517
|
|
|
7947
8518
|
/* ---------- rail (IDE-style left action strip) ---------- */
|
|
@@ -8180,7 +8751,7 @@ window.__ModuleLoader__.load({
|
|
|
8180
8751
|
E('div', { className: 'dig-compact-bar' },
|
|
8181
8752
|
E(Segmented, {
|
|
8182
8753
|
value: view === 'detail' ? 'history' : view,
|
|
8183
|
-
onChange: (next) => { setView(next); setPatch('') },
|
|
8754
|
+
onChange: (next) => { setView(next); setPatch(''); setDiffContext(null) },
|
|
8184
8755
|
items: [
|
|
8185
8756
|
{ id: 'changes', label: fill(t('seg.changes'), { n: dirty }) },
|
|
8186
8757
|
{ id: 'history', label: t('seg.history') },
|