dsh-ide-git 0.25.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
16
16
  - **想指给 AI 一段代码**:在 diff 窗里选中几行、右键,把「哪个文件哪几行」(附提交或工作区上下文)直接发进当前会话的输入框。
17
17
  - **想比较分支**:分支右键即可与当前分支比较,结果按文件与提交数汇报。
18
18
  - **操作要够全**:签出、从任意分支/提交新建分支、重命名、删除、合并到当前、变基当前到此、新建标签、cherry-pick、还原提交、重置(保留/丢弃)、fetch / pull --ff-only / push(确认后)。
19
- - **输入框里就能看 Git**:输入框工具条上有仓库与分支胶囊(着色的未提交 / 可拉取 / 待推送计数),点击即可切换,多仓库工作区两边都能切;切换与面板实时联动,设置里可关(默认开)。切到新会话也不用等——插件会在你点开会话时就预热首屏数据,面板秒开。
19
+ - **输入框里就能看 Git**:输入框工具条上有仓库与分支胶囊(着色的未提交 / 可拉取 / 待推送计数),点击弹出可搜索的切换器——仓库按名称/路径过滤,分支按 `/` 折成文件夹树(`fix/xxx` 一眼看清),输入即滤、方向键 + Enter 可选;多仓库工作区两边都能切,切换与面板实时联动,设置里可关(默认开)。切到新会话也不用等——插件会在你点开会话时就预热首屏数据,面板秒开。
20
20
 
21
21
  ## 效果
22
22
 
@@ -85,19 +85,22 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
85
85
 
86
86
  | 面 | 形态 | 本插件的布局 |
87
87
  |---|---|---|
88
- | 右侧栏(原生的,或底座桥接过来的) | 窄而高 | `stack`:变更区在提交历史之上,分支树可收起,详情/差异占满主区 |
88
+ | 右侧栏(原生的,或底座桥接过来的) | 窄而高 | `stack`:变更区在上,下方是「历史 / 差异」双页签(点变更文件自动切到差异),分支树可收起 |
89
89
  | 底部工作台(底座自有) | 宽而扁 | `columns`:分支树 · 提交图谱 · 变更与提交框,三栏并排——就是 JetBrains Git 工具窗口的样子 |
90
+ | 侧栏被拖得很窄(< 400px) | 窄 | `compact`:变更列表在上,下方是「历史 / 差异」双页签,分支树可收起 |
90
91
 
91
- 插件不猜自己在哪儿,而是用 `ResizeObserver` 量自己的容器:宽 ≥ 600px 且宽 ≥ 高 × 1.15 走三栏,否则走纵向堆叠(宽度说了算,高度没有否决权)。自由浮窗、移动端抽屉同样自适应。
92
+ 插件不猜自己在哪儿,而是用 `ResizeObserver` 量自己的容器:宽 ≥ 600px 且宽 ≥ 高 × 1.15 走三栏,宽 < 400px 走紧凑页签,其余走纵向堆叠(宽度说了算,高度没有否决权);两种纵向布局都把「历史 / 差异」做成页签,点变更文件自动切到差异。自由浮窗、移动端抽屉同样自适应。
92
93
 
93
94
  ## 功能一览
94
95
 
95
96
  | 区域 | 能力 |
96
97
  |---|---|
97
98
  | 动作条 | IDEA 式左侧竖排(底部工作台)/ 顶栏下一行横排(右侧栏):刷新、新建分支、签出、删除、比较、显示差异、贮藏、新建标签、收藏、抓取、拉取、推送。**按可用空间自动决定放几个**,放不下的收进 `⋯ 更多`;末尾 `⚙ 设置` 可拖动排序、逐项显隐(存 `dsh-ide-git.rail.v1`)。每个按钮按当前状态点亮/置灰(没有其他分支就不能删除/签出/比较,没有远程就不能抓取/推送,没有上游就不能拉取,没有变更就没有差异可看) |
99
+ | 窄栏页签 | 宽度 < 600px 的两种纵向布局里:变更列表固定在上方,下方是「历史 / 差异」两个页签——点变更文件自动切到「差异」,历史列表与它的滚动位置都不再被顶走;两区之间的分隔条可拖动、双击复位(尺寸与 `dsh-ide-git.panes.v1` 其它面板同一份存储) |
98
100
  | 悬停提示 | 鼠标停在图标按钮上 120ms,指针旁弹出它的操作名——竖排动作条里在图标右侧(跟着该行垂直居中,贴窗口右缘时翻到左侧),其余在下方;置灰的动作连原因一起给出(如 `隐藏 · 当前状态不可用`)。提示卡挂在窗口层,底部工作台裁不到;扫过一排图标不闪,按住鼠标拖拽 / 滚动 / 缩放时立即收起 |
99
101
  | 状态行 | 当前分支、上游、`↑ahead ↓behind`、stash 数量、忙碌指示 |
100
102
  | 输入框状态条 | 宿主输入框工具条里的仓库/分支胶囊(着色 ±未提交 / ↓可拉取 / ↑待推送),点击切换仓库与分支,与面板双向联动;单仓库只切分支,多仓库两边都切;设置可关(默认开) |
103
+ | 仓库/分支切换器 | 面板顶部与输入框胶囊共用同一个可搜索选择器:仓库按名称、路径、克隆目录或当前分支过滤,并标出各仓当前分支;分支按 `/` 折成文件夹树,输入时自动展开命中的目录,方向键 + Enter 可选,过滤器为空时目录可手动折叠 |
101
104
  | 分支树 | `HEAD / 本地 / 远程 / 标签` 分组(可折叠)、过滤框、星标当前分支、ahead/behind 角标、worktree 占用标记;本地蓝 / 远程紫 / 标签黄 / HEAD 绿 |
102
105
  | 工作树 | 动作条新增「工作树」视图:列出主工作树与全部链接工作树(当前 / 主工作树 / 已锁定 / 可清理标记、分支、短 HEAD、路径),可新建(新分支或挂已有分支,可指定起点)、在面板中打开、复制路径、移除(有未提交改动需二次强制确认)、清理失效记录;与官方「变更请求」面板并存 |
103
106
  | 分支右键 | 签出、将当前分支变基到此、合并到当前分支、与当前分支比较、从该分支新建分支、重命名、删除(当前分支禁用)、更新(Fetch)、推送、发送到对话(分支名 + 仓库) |
package/README_EN.md CHANGED
@@ -75,17 +75,20 @@ The same panel shows up on surfaces of very different shapes, so the layout meas
75
75
 
76
76
  | Surface | Shape | Layout here |
77
77
  |---|---|---|
78
- | Right sidebar (native, or bridged by better-sidebar) | narrow + tall | `stack`: changes above the graph, collapsible branch pane |
78
+ | Right sidebar (native, or bridged by better-sidebar) | narrow + tall | `stack`: changes on top, history and diff as two tabs below (a click on a file switches to `Diff`), collapsible branch pane |
79
79
  | Bottom workbench (better-sidebar only) | wide + flat | `columns`: tree / graph / changes — the JetBrains Git tool window shape |
80
+ | Sidebar dragged very narrow (< 400px) | narrow | `compact`: changes on top, history and diff as two tabs below, collapsible tree |
80
81
 
81
- The panel never guesses: it measures itself with a `ResizeObserver` (width ≥ 600px and width ≥ 1.15 × height → columns, otherwise stack — width decides, height has no veto), so free-floating windows and the mobile drawer adapt too.
82
+ The panel never guesses: it measures itself with a `ResizeObserver` (width ≥ 600px and width ≥ 1.15 × height → columns, width < 400px → the compact tabs, otherwise stack — width decides, height has no veto); both stacked layouts make history and diff tabs, and a click on a changed file flips to `Diff`. Free-floating windows and the mobile drawer adapt too.
82
83
 
83
84
  ## Features
84
85
 
85
86
  - 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).
87
+ - Tabs in the stacked layouts: below 600px (both `stack` and `compact`) the changes list keeps the top of the panel and the lower window is a `History` / `Diff` tab pair — opening a changed file switches to `Diff`, so the history list (and its scroll position) is never pushed away; the divider above that window drags and double-click resets (same store as the other panes, `dsh-ide-git.panes.v1`).
86
88
  - Hover hints: rest the pointer on an icon button for 120ms and its action name appears beside it — to the RIGHT of a vertical rail icon (centred on that row, flipping to the left when the window's right edge is in the way) and below everywhere else; greyed-out actions explain themselves too (e.g. `Hide · Not available in the current state`). The hint card lives on the window layer, so the bottom workbench cannot clip it — sweeping across a row of icons does not flash, and a pressed pointer / scroll / resize drops it immediately.
87
89
  - Status line: branch, upstream, ahead/behind, stash count, busy indicator.
88
- - Composer status pill: repository/branch capsule in the host input-bar tool row (colored uncommitted / pullable / to-push counters), click to switch repo and branch — kept in sync with the panel both ways; single-repo workspaces switch branches only, multi-repo workspaces switch both. Off-switch in the settings (on by default). Sessions are pre-warmed on switch so the panel opens instantly.
90
+ - Composer status pill: repository/branch capsule in the host input-bar tool row (colored uncommitted / pullable / to-push counters), click to open a searchable switcher — repositories filter by name or path, branches fold on `/` into a folder tree (`fix/xxx` stays legible), typing filters immediately, arrow keys + Enter pick. Kept in sync with the panel both ways; single-repo workspaces switch branches only, multi-repo workspaces switch both. Off-switch in the settings (on by default). Sessions are pre-warmed on switch so the panel opens instantly.
91
+ - Repo/branch switcher: the panel header and the composer pills share one searchable picker. Repositories filter by name, path, clone directory or current branch and show each clone's checked-out branch; branches fold on `/` into a folder tree that opens matching folders while typing, arrow keys + Enter pick, and folders stay collapsible on an empty filter.
89
92
  - Branch tree: HEAD / Local / Remote / Tags groups, filter box, ahead/behind badges, worktree marker; local blue / remote violet / tag amber / HEAD green.
90
93
  - Worktrees: a rail view listing the primary and every linked worktree (current / primary / locked / prunable badges, branch, short HEAD, path). Create one on a new branch or attach an existing branch (with an optional start point), open it in the panel, copy its path, remove it (a dirty tree asks for a second, forced confirmation), and prune stale records. It coexists with the official change-request panel.
91
94
  - 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).
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.25.0",
3
+ "version": "0.27.1",
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.25.0",
3
+ "version": "0.27.1",
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
@@ -9,8 +9,8 @@
9
9
  * chrome:
10
10
  *
11
11
  * columns wide and tall enough : repo/branch bar | tree | graph | changes
12
- * stack narrow and tall : changes over graph, collapsible tree
13
- * compact short or very narrow : one header row + a Changes/History switch
12
+ * stack narrow and tall : changes over a tabbed history/diff window
13
+ * compact very narrow : changes on top + the same tabbed window
14
14
  *
15
15
  * Theming: no hard-coded surfaces. Every surface is a --dsw-alias-* token, so
16
16
  * transparent themes and background plugins (dsh-any-background) show through,
@@ -90,6 +90,8 @@ window.__ModuleLoader__.load({
90
90
  'repo.nested': '子仓库',
91
91
  'repo.none': '当前工作区不是 Git 仓库',
92
92
  'repo.pick': '选择要查看的仓库',
93
+ 'repo.filter': '搜索仓库',
94
+ 'picker.empty': '没有匹配项',
93
95
  'repo.empty': '这个工作区下没有找到 Git 仓库',
94
96
  'repo.switch': '切换仓库',
95
97
  'branch.switch': '切换分支',
@@ -462,6 +464,8 @@ window.__ModuleLoader__.load({
462
464
  'repo.nested': 'nested',
463
465
  'repo.none': 'This workspace is not a Git repository',
464
466
  'repo.pick': 'Pick a repository to inspect',
467
+ 'repo.filter': 'Search repositories',
468
+ 'picker.empty': 'No matches',
465
469
  'repo.empty': 'No Git repository found in this workspace',
466
470
  'repo.switch': 'Switch repository',
467
471
  'branch.switch': 'Switch branch',
@@ -842,6 +846,8 @@ window.__ModuleLoader__.load({
842
846
  'repo.nested': '巢狀',
843
847
  'repo.none': '呢個工作區唔係 Git 存放庫',
844
848
  'repo.pick': '揀一個存放庫嚟睇',
849
+ 'repo.filter': '搵存放庫',
850
+ 'picker.empty': '冇相符項目',
845
851
  'repo.empty': '呢個工作區搵唔到 Git 存放庫',
846
852
  'repo.switch': '切換存放庫',
847
853
  'branch.switch': '切換分支',
@@ -1214,6 +1220,8 @@ window.__ModuleLoader__.load({
1214
1220
  'repo.nested': '巢狀',
1215
1221
  'repo.none': '這個工作區不是 Git 版本庫',
1216
1222
  'repo.pick': '選擇要檢視的版本庫',
1223
+ 'repo.filter': '搜尋版本庫',
1224
+ 'picker.empty': '沒有相符項目',
1217
1225
  'repo.empty': '這個工作區找不到 Git 版本庫',
1218
1226
  'repo.switch': '切換版本庫',
1219
1227
  'branch.switch': '切換分支',
@@ -1586,6 +1594,8 @@ window.__ModuleLoader__.load({
1586
1594
  'repo.nested': '巢狀',
1587
1595
  'repo.none': '呢個工作區唔係 Git 存放庫',
1588
1596
  'repo.pick': '揀一個存放庫嚟睇',
1597
+ 'repo.filter': '搵存放庫',
1598
+ 'picker.empty': '冇相符項目',
1589
1599
  'repo.empty': '呢個工作區搵唔到 Git 存放庫',
1590
1600
  'repo.switch': '切換存放庫',
1591
1601
  'branch.switch': '切換分支',
@@ -1958,6 +1968,8 @@ window.__ModuleLoader__.load({
1958
1968
  'repo.nested': 'ネスト',
1959
1969
  'repo.none': 'このワークスペースは Git リポジトリではありません',
1960
1970
  'repo.pick': '確認するリポジトリを選択',
1971
+ 'repo.filter': 'リポジトリを検索',
1972
+ 'picker.empty': '一致する項目がありません',
1961
1973
  'repo.empty': 'このワークスペースに Git リポジトリが見つかりません',
1962
1974
  'repo.switch': 'リポジトリを切り替え',
1963
1975
  'branch.switch': 'ブランチを切り替え',
@@ -2330,6 +2342,8 @@ window.__ModuleLoader__.load({
2330
2342
  'repo.nested': '중첩',
2331
2343
  'repo.none': '이 워크스페이스는 Git 저장소가 아닙니다',
2332
2344
  'repo.pick': '확인할 저장소를 선택하세요',
2345
+ 'repo.filter': '저장소 검색',
2346
+ 'picker.empty': '일치하는 항목이 없습니다',
2333
2347
  'repo.empty': '이 워크스페이스에서 Git 저장소를 찾을 수 없습니다',
2334
2348
  'repo.switch': '저장소 전환',
2335
2349
  'branch.switch': '브랜치 전환',
@@ -2702,6 +2716,8 @@ window.__ModuleLoader__.load({
2702
2716
  'repo.nested': 'verschachtelt',
2703
2717
  'repo.none': 'Dieser Arbeitsbereich ist kein Git-Repository',
2704
2718
  'repo.pick': 'Repository zum Ansehen wählen',
2719
+ 'repo.filter': 'Repositories durchsuchen',
2720
+ 'picker.empty': 'Keine Treffer',
2705
2721
  'repo.empty': 'In diesem Arbeitsbereich wurde kein Git-Repository gefunden',
2706
2722
  'repo.switch': 'Repository wechseln',
2707
2723
  'branch.switch': 'Branch wechseln',
@@ -3074,6 +3090,8 @@ window.__ModuleLoader__.load({
3074
3090
  'repo.nested': 'imbriqué',
3075
3091
  'repo.none': 'Cet espace de travail n\'est pas un dépôt Git',
3076
3092
  'repo.pick': 'Choisir un dépôt à inspecter',
3093
+ 'repo.filter': 'Rechercher des dépôts',
3094
+ 'picker.empty': 'Aucun résultat',
3077
3095
  'repo.empty': 'Aucun dépôt Git trouvé dans cet espace de travail',
3078
3096
  'repo.switch': 'Changer de dépôt',
3079
3097
  'branch.switch': 'Changer de branche',
@@ -3446,6 +3464,8 @@ window.__ModuleLoader__.load({
3446
3464
  'repo.nested': 'вложенный',
3447
3465
  'repo.none': 'Эта рабочая область не является репозиторием Git',
3448
3466
  'repo.pick': 'Выберите репозиторий для просмотра',
3467
+ 'repo.filter': 'Поиск репозиториев',
3468
+ 'picker.empty': 'Нет совпадений',
3449
3469
  'repo.empty': 'В этой рабочей области репозиторий Git не найден',
3450
3470
  'repo.switch': 'Сменить репозиторий',
3451
3471
  'branch.switch': 'Сменить ветку',
@@ -3818,6 +3838,8 @@ window.__ModuleLoader__.load({
3818
3838
  'repo.nested': 'aninhado',
3819
3839
  'repo.none': 'Esta área de trabalho não é um repositório Git',
3820
3840
  'repo.pick': 'Escolha um repositório para inspecionar',
3841
+ 'repo.filter': 'Pesquisar repositórios',
3842
+ 'picker.empty': 'Nenhum resultado',
3821
3843
  'repo.empty': 'Nenhum repositório Git encontrado nesta área de trabalho',
3822
3844
  'repo.switch': 'Trocar de repositório',
3823
3845
  'branch.switch': 'Trocar de branch',
@@ -4190,6 +4212,8 @@ window.__ModuleLoader__.load({
4190
4212
  'repo.nested': 'annidato',
4191
4213
  'repo.none': 'Questa area di lavoro non è un repository Git',
4192
4214
  'repo.pick': 'Scegli un repository da esaminare',
4215
+ 'repo.filter': 'Cerca repository',
4216
+ 'picker.empty': 'Nessuna corrispondenza',
4193
4217
  'repo.empty': 'Nessun repository Git trovato in questa area di lavoro',
4194
4218
  'repo.switch': 'Cambia repository',
4195
4219
  'branch.switch': 'Cambia branch',
@@ -4562,6 +4586,8 @@ window.__ModuleLoader__.load({
4562
4586
  'repo.nested': 'genest',
4563
4587
  'repo.none': 'Deze werkruimte is geen Git-repository',
4564
4588
  'repo.pick': 'Kies een repository om te bekijken',
4589
+ 'repo.filter': 'Repositories zoeken',
4590
+ 'picker.empty': 'Geen resultaten',
4565
4591
  'repo.empty': 'Geen Git-repository gevonden in deze werkruimte',
4566
4592
  'repo.switch': 'Wissel van repository',
4567
4593
  'branch.switch': 'Wissel van branch',
@@ -4934,6 +4960,8 @@ window.__ModuleLoader__.load({
4934
4960
  'repo.nested': 'zagnieżdżone',
4935
4961
  'repo.none': 'Ten obszar roboczy nie jest repozytorium Git',
4936
4962
  'repo.pick': 'Wybierz repozytorium do podglądu',
4963
+ 'repo.filter': 'Szukaj repozytoriów',
4964
+ 'picker.empty': 'Brak dopasowań',
4937
4965
  'repo.empty': 'W tym obszarze roboczym nie znaleziono repozytorium Git',
4938
4966
  'repo.switch': 'Zmień repozytorium',
4939
4967
  'branch.switch': 'Zmień gałąź',
@@ -5306,6 +5334,8 @@ window.__ModuleLoader__.load({
5306
5334
  'repo.nested': 'nästlad',
5307
5335
  'repo.none': 'Den här arbetsytan är inte ett Git-arkiv',
5308
5336
  'repo.pick': 'Välj ett arkiv att granska',
5337
+ 'repo.filter': 'Sök bland arkiv',
5338
+ 'picker.empty': 'Inga träffar',
5309
5339
  'repo.empty': 'Inget Git-arkiv hittades i den här arbetsytan',
5310
5340
  'repo.switch': 'Byt arkiv',
5311
5341
  'branch.switch': 'Byt gren',
@@ -5678,6 +5708,8 @@ window.__ModuleLoader__.load({
5678
5708
  'repo.nested': 'iç içe',
5679
5709
  'repo.none': 'Bu çalışma alanı bir Git deposu değil',
5680
5710
  'repo.pick': 'İncelenecek depoyu seçin',
5711
+ 'repo.filter': 'Depo ara',
5712
+ 'picker.empty': 'Eşleşme yok',
5681
5713
  'repo.empty': 'Bu çalışma alanında Git deposu bulunamadı',
5682
5714
  'repo.switch': 'Depoyu değiştir',
5683
5715
  'branch.switch': 'Dalı değiştir',
@@ -6050,6 +6082,8 @@ window.__ModuleLoader__.load({
6050
6082
  'repo.nested': 'bersarang',
6051
6083
  'repo.none': 'Ruang kerja ini bukan repositori Git',
6052
6084
  'repo.pick': 'Pilih repositori untuk diperiksa',
6085
+ 'repo.filter': 'Cari repositori',
6086
+ 'picker.empty': 'Tidak ada kecocokan',
6053
6087
  'repo.empty': 'Tidak ada repositori Git di ruang kerja ini',
6054
6088
  'repo.switch': 'Ganti repositori',
6055
6089
  'branch.switch': 'Ganti branch',
@@ -6422,6 +6456,8 @@ window.__ModuleLoader__.load({
6422
6456
  'repo.nested': 'lồng nhau',
6423
6457
  'repo.none': 'Không gian làm việc này không phải kho Git',
6424
6458
  'repo.pick': 'Chọn một kho để xem',
6459
+ 'repo.filter': 'Tìm kho',
6460
+ 'picker.empty': 'Không có kết quả phù hợp',
6425
6461
  'repo.empty': 'Không tìm thấy kho Git trong không gian làm việc này',
6426
6462
  'repo.switch': 'Chuyển kho',
6427
6463
  'branch.switch': 'Chuyển nhánh',
@@ -6794,6 +6830,8 @@ window.__ModuleLoader__.load({
6794
6830
  'repo.nested': 'متشعّب',
6795
6831
  'repo.none': 'مساحة العمل هذه ليست مستودع Git',
6796
6832
  'repo.pick': 'اختر مستودعًا لعرضه',
6833
+ 'repo.filter': 'البحث عن المستودعات',
6834
+ 'picker.empty': 'لا توجد نتائج مطابقة',
6797
6835
  'repo.empty': 'لم يُعثر على مستودع Git في مساحة العمل هذه',
6798
6836
  'repo.switch': 'تبديل المستودع',
6799
6837
  'branch.switch': 'تبديل الفرع',
@@ -7166,6 +7204,8 @@ window.__ModuleLoader__.load({
7166
7204
  'repo.nested': 'नेस्टेड',
7167
7205
  'repo.none': 'यह कार्यक्षेत्र Git रिपॉज़िटरी नहीं है',
7168
7206
  'repo.pick': 'देखने के लिए रिपॉज़िटरी चुनें',
7207
+ 'repo.filter': 'रिपॉज़िटरी खोजें',
7208
+ 'picker.empty': 'कोई मेल नहीं',
7169
7209
  'repo.empty': 'इस कार्यक्षेत्र में कोई Git रिपॉज़िटरी नहीं मिली',
7170
7210
  'repo.switch': 'रिपॉज़िटरी बदलें',
7171
7211
  'branch.switch': 'शाखा बदलें',
@@ -7538,6 +7578,8 @@ window.__ModuleLoader__.load({
7538
7578
  'repo.nested': 'ซ้อนกัน',
7539
7579
  'repo.none': 'เวิร์กสเปซนี้ไม่ใช่รีโพซิทอรี Git',
7540
7580
  'repo.pick': 'เลือกรีโพซิทอรีที่จะดู',
7581
+ 'repo.filter': 'ค้นหารีโพซิทอรี',
7582
+ 'picker.empty': 'ไม่มีรายการที่ตรงกัน',
7541
7583
  'repo.empty': 'ไม่พบรีโพซิทอรี Git ในเวิร์กสเปซนี้',
7542
7584
  'repo.switch': 'สลับรีโพซิทอรี',
7543
7585
  'branch.switch': 'สลับแบรนช์',
@@ -8852,8 +8894,12 @@ window.__ModuleLoader__.load({
8852
8894
  panes measure width (columns). */
8853
8895
  const PANE_CHROME_KEYS = {
8854
8896
  columns: ['tree', 'changes', 'diff'],
8855
- stack: ['tree', 'changes', 'diff'],
8856
- compact: ['tree', 'diff'],
8897
+ /* v0.26.1: BOTH stacked chromes put the history list and the diff in one
8898
+ TABBED window (history | diff) under the changes list, so the diff is
8899
+ no longer a pane of its own here — it fills the tab body. Only the
8900
+ three-column chrome still splits the diff out of the middle column. */
8901
+ stack: ['tree', 'changes'],
8902
+ compact: ['tree', 'changes'],
8857
8903
  }
8858
8904
  /* Whether a key measures a height (stacked chromes) or a width (columns).
8859
8905
  This is the chrome's DEFAULT for the tree/changes panes; the diff pane
@@ -8869,9 +8915,8 @@ window.__ModuleLoader__.load({
8869
8915
  'columns:diff': { min: 120, max: 900 },
8870
8916
  'stack:tree': { min: 100, max: 420 },
8871
8917
  'stack:changes': { min: 120, max: 460 },
8872
- 'stack:diff': { min: 120, max: 900 },
8873
8918
  'compact:tree': { min: 100, max: 420 },
8874
- 'compact:diff': { min: 120, max: 900 },
8919
+ 'compact:changes': { min: 120, max: 460 },
8875
8920
  }
8876
8921
  /* Today's fixed sizes stay the default — and stay in the stylesheet: a pane
8877
8922
  the user never dragged keeps exactly the look it had before, while a
@@ -8884,6 +8929,7 @@ window.__ModuleLoader__.load({
8884
8929
  'stack:tree': { ratio: 0.36 },
8885
8930
  'stack:changes': { ratio: 0.46 },
8886
8931
  'compact:tree': { ratio: 0.42 },
8932
+ 'compact:changes': { ratio: 0.46 },
8887
8933
  }
8888
8934
  const PANE_MAIN_MIN_W = 240
8889
8935
  const PANE_MAIN_MIN_H = 150
@@ -8899,7 +8945,7 @@ window.__ModuleLoader__.load({
8899
8945
  history↔diff separator lives inside the middle pane, so it is not counted
8900
8946
  against the body's own budget. */
8901
8947
  const PANE_GUTTER_PX = 8
8902
- const PANE_BODY_GUTTERS = { columns: 2, stack: 2, compact: 1 }
8948
+ const PANE_BODY_GUTTERS = { columns: 2, stack: 2, compact: 2 }
8903
8949
  const PANE_BUCKETS = Object.keys(PANE_CHROME_KEYS).reduce(
8904
8950
  (all, chrome) => all.concat([chrome + ':wide', chrome + ':tall']), [])
8905
8951
 
@@ -10469,12 +10515,187 @@ window.__ModuleLoader__.load({
10469
10515
  return E('div', { className: 'dig-menu-wrap', ref: ref, style: { left: position.left + 'px', top: position.top + 'px' } },
10470
10516
  E('div', { className: 'dig-menu', ref: cardRef, 'data-menu-material': 'translucent' },
10471
10517
  E('div', { className: 'dig-menu-scroll' },
10472
- props.items.map((item, index) => renderItem(item, index, false)))),
10518
+ props.body === undefined
10519
+ ? (Array.isArray(props.items) ? props.items : []).map((item, index) => renderItem(item, index, false))
10520
+ : props.body)),
10473
10521
  sub === null ? null : E('div', { className: 'dig-menu dig-menu-sub', 'data-menu-material': 'translucent', style: sub.flip === true ? { right: '100%', top: sub.top + 'px' } : { left: '100%', top: sub.top + 'px' } },
10474
10522
  E('div', { className: 'dig-menu-scroll' },
10475
10523
  sub.items.map((item, index) => renderItem(item, index, true)))))
10476
10524
  }
10477
10525
 
10526
+ /* Searchable picker body shared by the panel header switchers and the
10527
+ composer pills. It owns no positioning: the panel renders it as a
10528
+ ContextMenu body, the composer inside its own .dig-cs-menu. Repositories
10529
+ are a flat, filtered list; branches keep their slash namespace as a
10530
+ collapsible tree, and a non-empty query opens every folder so a match
10531
+ is never hidden behind a fold. */
10532
+ function SearchPicker(props) {
10533
+ const t = props.t
10534
+ const mode = props.mode === 'branch' ? 'branch' : 'repo'
10535
+ const [needle, setNeedle] = useState('')
10536
+ const [folded, setFolded] = useState({})
10537
+ const [active, setActive] = useState(0)
10538
+ const inputRef = useRef(null)
10539
+ const listRef = useRef(null)
10540
+ useEffect(() => {
10541
+ const input = inputRef.current
10542
+ if (input !== null && typeof input.focus === 'function') input.focus({ preventScroll: true })
10543
+ }, [])
10544
+ const query = needle.trim().toLowerCase()
10545
+ const matches = (value) => query === '' || String(value === undefined || value === null ? '' : value).toLowerCase().indexOf(query) >= 0
10546
+ const rows = []
10547
+ if (mode === 'repo') {
10548
+ const repos = Array.isArray(props.repos) ? props.repos : []
10549
+ for (const repo of repos) {
10550
+ const name = repo.name === undefined || repo.name === null ? baseNameOf(repo.path) : String(repo.name)
10551
+ if (query !== '' && matches(name) === false && matches(repo.path) === false && matches(repo.sub) === false && matches(repo.branch) === false) continue
10552
+ rows.push({ key: 'repo:' + String(repo.path), kind: 'repo', repo: repo, name: name })
10553
+ }
10554
+ } else {
10555
+ const entries = (Array.isArray(props.branches) ? props.branches : []).filter((entry) => matches(entry.name))
10556
+ const root = { folders: new Map(), leaves: [] }
10557
+ for (const entry of entries) {
10558
+ const parts = String(entry.name).split('/')
10559
+ let node = root
10560
+ for (let index = 0; index < parts.length - 1; index += 1) {
10561
+ const key = parts[index]
10562
+ if (node.folders.has(key) === false) node.folders.set(key, { folders: new Map(), leaves: [] })
10563
+ node = node.folders.get(key)
10564
+ }
10565
+ node.leaves.push({ entry: entry, label: parts[parts.length - 1] })
10566
+ }
10567
+ const countLeaves = (node) => node.leaves.length + Array.from(node.folders.values()).reduce((sum, child) => sum + countLeaves(child), 0)
10568
+ const walk = (node, prefix, depth) => {
10569
+ for (const name of Array.from(node.folders.keys()).sort()) {
10570
+ const id = prefix === '' ? name : prefix + '/' + name
10571
+ const open = query !== '' || folded[id] !== true
10572
+ rows.push({ key: 'dir:' + id, kind: 'folder', id: id, name: name, depth: depth, count: countLeaves(node.folders.get(name)), open: open })
10573
+ if (open === true) walk(node.folders.get(name), id, depth + 1)
10574
+ }
10575
+ for (const leaf of node.leaves) {
10576
+ const entry = leaf.entry
10577
+ const worktree = entry.worktree !== null && entry.worktree !== undefined
10578
+ const busy = typeof props.busyName === 'string' && props.busyName !== ''
10579
+ rows.push({
10580
+ key: 'branch:' + String(entry.name), kind: 'branch', entry: entry, label: leaf.label, depth: depth,
10581
+ disabled: worktree === true || busy === true, worktree: worktree,
10582
+ })
10583
+ }
10584
+ }
10585
+ walk(root, '', 0)
10586
+ }
10587
+ const selectable = rows.filter((row) => row.kind !== 'folder' && row.disabled !== true)
10588
+ const activeIndex = selectable.length === 0 ? -1 : Math.min(active, selectable.length - 1)
10589
+ const activeRow = activeIndex < 0 ? null : selectable[activeIndex]
10590
+ const activeKey = activeRow === null ? '' : activeRow.key
10591
+ useEffect(() => {
10592
+ const host = listRef.current
10593
+ if (host === null || activeKey === '') return
10594
+ const found = Array.from(host.querySelectorAll('[data-pick-key]')).find((row) => row.getAttribute('data-pick-key') === activeKey)
10595
+ if (found !== undefined && typeof found.scrollIntoView === 'function') found.scrollIntoView({ block: 'nearest' })
10596
+ }, [activeKey])
10597
+ const pickRow = (row) => {
10598
+ if (row === null || row.disabled === true || typeof props.onPick !== 'function') return
10599
+ if (row.kind === 'repo') props.onPick(row.repo.path, row.repo)
10600
+ else if (row.kind === 'branch') props.onPick(row.entry.name, row.entry)
10601
+ }
10602
+ const onKeyDown = (event) => {
10603
+ if (event.key === 'Escape') {
10604
+ event.preventDefault()
10605
+ event.stopPropagation()
10606
+ if (typeof props.onClose === 'function') props.onClose()
10607
+ return
10608
+ }
10609
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
10610
+ if (selectable.length === 0) return
10611
+ event.preventDefault()
10612
+ event.stopPropagation()
10613
+ setActive((value) => {
10614
+ const current = Math.min(value, selectable.length - 1)
10615
+ return event.key === 'ArrowDown' ? (current + 1) % selectable.length : (current - 1 + selectable.length) % selectable.length
10616
+ })
10617
+ return
10618
+ }
10619
+ if (event.key === 'Enter') {
10620
+ event.preventDefault()
10621
+ event.stopPropagation()
10622
+ pickRow(activeRow)
10623
+ }
10624
+ }
10625
+ const currentRepo = mode === 'repo' && props.current !== undefined && props.current !== null ? String(props.current) : ''
10626
+ const currentBranch = mode === 'branch' && props.current !== undefined && props.current !== null ? String(props.current) : ''
10627
+ const rowClass = (row) => 'dig-pick-row'
10628
+ + (activeKey === row.key ? ' dig-pick-row-active' : '')
10629
+ + ((currentRepo !== '' && row.kind === 'repo' && String(row.repo.path) === currentRepo) ? ' dig-pick-row-current' : '')
10630
+ + ((currentBranch !== '' && row.kind === 'branch' && String(row.entry.name) === currentBranch) ? ' dig-pick-row-current' : '')
10631
+ + (row.disabled === true ? ' dig-pick-row-disabled' : '')
10632
+ const rowsView = rows.map((row) => {
10633
+ if (row.kind === 'folder') {
10634
+ return E('button', {
10635
+ key: row.key, type: 'button',
10636
+ className: 'dig-pick-folder' + (row.open !== true ? ' dig-pick-folder-closed' : ''),
10637
+ style: { paddingLeft: (6 + row.depth * 14) + 'px' },
10638
+ onClick: () => { if (query !== '') return; setFolded((previous) => Object.assign({}, previous, { [row.id]: row.open === true })) },
10639
+ },
10640
+ E('span', { className: 'dig-pick-chevron' + (row.open === true ? ' dig-pick-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
10641
+ E(Icon, { name: 'folder', size: 12 }),
10642
+ E('span', { className: 'dig-pick-name' }, row.name),
10643
+ E('span', { className: 'dig-pick-count' }, String(row.count)))
10644
+ }
10645
+ if (row.kind === 'repo') {
10646
+ return E('button', {
10647
+ key: row.key, type: 'button', className: rowClass(row), 'data-pick-key': row.key,
10648
+ title: row.repo.path,
10649
+ onMouseEnter: () => { const index = selectable.indexOf(row); if (index >= 0) setActive(index) },
10650
+ onClick: () => pickRow(row),
10651
+ },
10652
+ E(Icon, { name: 'folder', size: 13 }),
10653
+ E('span', { className: 'dig-pick-main' },
10654
+ E('span', { className: 'dig-pick-name' }, row.name),
10655
+ row.repo.sub !== undefined && row.repo.sub !== null && row.repo.sub !== '' && row.repo.sub !== row.name
10656
+ ? E('span', { className: 'dig-pick-sub' }, row.repo.sub)
10657
+ : null),
10658
+ row.repo.branch === undefined || row.repo.branch === null || row.repo.branch === ''
10659
+ ? null
10660
+ : E('span', { className: 'dig-pick-badge' }, row.repo.branch))
10661
+ }
10662
+ const entry = row.entry
10663
+ const head = entry.head === true
10664
+ const worktreeNote = row.worktree === true
10665
+ ? (props.worktreeNote === undefined ? t('branches.worktree') : props.worktreeNote)
10666
+ : null
10667
+ return E('button', {
10668
+ key: row.key, type: 'button',
10669
+ className: rowClass(row) + (head === true ? ' dig-pick-row-head' : ''),
10670
+ 'data-pick-key': row.key,
10671
+ style: { paddingLeft: (6 + row.depth * 14) + 'px' },
10672
+ disabled: row.disabled === true,
10673
+ title: String(entry.name) + (worktreeNote === null ? '' : ' · ' + worktreeNote),
10674
+ onMouseEnter: () => { const index = selectable.indexOf(row); if (index >= 0) setActive(index) },
10675
+ onClick: () => pickRow(row),
10676
+ },
10677
+ E('span', { className: 'dig-pick-icon dig-tone-' + (head === true ? 'warn' : 'accent') }, E(Icon, { name: head === true ? 'star' : 'branch', size: 12 })),
10678
+ E('span', { className: 'dig-pick-name' }, row.label),
10679
+ worktreeNote === null ? null : E('span', { className: 'dig-pick-note' }, worktreeNote),
10680
+ entry.ahead > 0 ? E('span', { className: 'dig-pick-count' }, '↑' + String(entry.ahead)) : null,
10681
+ entry.behind > 0 ? E('span', { className: 'dig-pick-count' }, '↓' + String(entry.behind)) : null)
10682
+ })
10683
+ return E('div', { className: 'dig-pick dig-pick-' + mode },
10684
+ E('div', { className: 'dig-pick-search' },
10685
+ E('input', {
10686
+ ref: inputRef,
10687
+ className: 'dig-input dig-input-compact dig-pick-input',
10688
+ value: needle,
10689
+ spellCheck: false,
10690
+ placeholder: mode === 'repo' ? t('repo.filter') : t('branches.filter'),
10691
+ 'aria-label': mode === 'repo' ? t('repo.filter') : t('branches.filter'),
10692
+ onChange: (event) => { setNeedle(event.target.value); setActive(0) },
10693
+ onKeyDown: onKeyDown,
10694
+ })),
10695
+ E('div', { className: 'dig-pick-list', ref: listRef, role: 'listbox' },
10696
+ rowsView.length === 0 ? E('div', { className: 'dig-pick-empty' }, t('picker.empty')) : rowsView))
10697
+ }
10698
+
10478
10699
  /* Lets the user choose which actions the rail shows and in which order. The
10479
10700
  list is drag-sortable and arrow-sortable (the arrows also work with a
10480
10701
  keyboard); visibility is a per-action toggle, not a delete, so a hidden
@@ -12027,10 +12248,12 @@ window.__ModuleLoader__.load({
12027
12248
  return
12028
12249
  }
12029
12250
  anchorRef.current = key
12030
- /* Plain click only SELECTS (single-replace, the branch tree's model):
12031
- the diff moved to DOUBLE-click — a stray click mid-multi-select must
12032
- not yank the pane away (v0.18.3). */
12251
+ /* Plain click SELECTS and shows the diff (v0.25.1, user request): in the
12252
+ narrow right column the list replaces the pane, so a click that only
12253
+ highlighted the row looked like "nothing happened". Modifier clicks
12254
+ above stay pure multi-select; ignored rows have no diff to show. */
12033
12255
  setSelection((previous) => previous.size === 1 && previous.has(key) === true ? previous : new Set([key]))
12256
+ if (group !== 'ignored' && props.onDiff !== undefined) props.onDiff(item, group)
12034
12257
  }, [setSelection, anchorRef, props.onDiff])
12035
12258
  /* Rubber-band selection now comes from the shared useRubberBand hook
12036
12259
  (see its comment): the fixed row heights + the prefix-sum offsets
@@ -13165,11 +13388,37 @@ window.__ModuleLoader__.load({
13165
13388
  const [selectedPath, setSelectedPath] = useState(null)
13166
13389
  const [patch, setPatch] = useState('')
13167
13390
  const [patchLoading, setPatchLoading] = useState(false)
13391
+ /* The compact chrome's lower window is tabbed (history | diff) instead of
13392
+ split, so opening a file does not take the history list away: the diff
13393
+ gets its own tab and the user can switch back (v0.26.0). */
13394
+ const [splitTab, setSplitTab] = useState('history')
13395
+ useEffect(() => {
13396
+ if (splitTab === 'diff' && patch === '' && patchLoading === false) setSplitTab('history')
13397
+ }, [splitTab, patch, patchLoading])
13398
+ /* v0.26.3: the row fires onDiff from its single click now, so a double
13399
+ click used to open the same diff three times — and the responses raced
13400
+ (a slow first request could land after the second and paint the wrong
13401
+ file under the new title). `diffSeq` stamps each request and only the
13402
+ newest one is allowed to write; `diffLast` swallows the burst a double
13403
+ click produces. */
13404
+ const diffSeq = useRef(0)
13405
+ const diffLast = useRef({ key: '', at: 0 })
13168
13406
  /* What the open diff pane shows (v0.13.5): the pane is shared by the
13169
13407
  commit detail (hash + subject) and the working tree (staged or not),
13170
13408
  and the selection menu's payload must say which. Cleared everywhere
13171
13409
  the patch is cleared. */
13172
13410
  const [diffContext, setDiffContext] = useState(null)
13411
+ /* v0.27: closing / switching away must also invalidate the request in
13412
+ flight and reset the double-click guard. Without the stamp bump a
13413
+ late response repainted the closed pane; without the diffLast reset
13414
+ reopening the same file within 400ms was swallowed. */
13415
+ const resetDiff = useCallback(() => {
13416
+ diffSeq.current += 1
13417
+ diffLast.current = { key: '', at: 0 }
13418
+ setPatch('')
13419
+ setPatchLoading(false)
13420
+ setDiffContext(null)
13421
+ }, [])
13173
13422
  /* ---- multi-select (issue #13): the change list's selection lives here
13174
13423
  so the right-click menu, the keyboard and the selection bar act on one
13175
13424
  set; the branch tree and the history keep theirs closer to their rows
@@ -13203,6 +13452,10 @@ window.__ModuleLoader__.load({
13203
13452
  const onChangeSelection = useCallback((update) => { setChangeSelection(update); clearPicksExcept('changes') }, [clearPicksExcept])
13204
13453
  const onCommitPicks = useCallback((update) => { setPickedCommits(update); clearPicksExcept('history') }, [clearPicksExcept])
13205
13454
  const [menu, setMenu] = useState(null)
13455
+ /* The header repo/branch switchers need a search box and a branch tree,
13456
+ which a plain ContextMenu item list cannot express; they share the
13457
+ same anchored surface through its `body` prop. */
13458
+ const [picker, setPicker] = useState(null)
13206
13459
  const [dialog, setDialog] = useState(null)
13207
13460
  const [tick, setTick] = useState(0)
13208
13461
  const [favTick, setFavTick] = useState(0)
@@ -13818,11 +14071,10 @@ window.__ModuleLoader__.load({
13818
14071
  headRef.current = null
13819
14072
  setDetail(null)
13820
14073
  setSelectedHash(null)
13821
- setPatch('')
13822
- setDiffContext(null)
14074
+ resetDiff()
13823
14075
  setView('history')
13824
14076
  setPathFilter('')
13825
- }, [sessionId])
14077
+ }, [sessionId, resetDiff])
13826
14078
 
13827
14079
  /* The composer status pill and this panel are separate trees; when the
13828
14080
  pill (or any other seat) switches the repository or the branch, this
@@ -13849,16 +14101,24 @@ window.__ModuleLoader__.load({
13849
14101
  }, [sessionId, repoRoot, pickRepo, refresh])
13850
14102
 
13851
14103
  const openDiff = useCallback(async (seed) => {
14104
+ const key = JSON.stringify([base, seed])
14105
+ const now = Date.now()
14106
+ if (key === diffLast.current.key && now - diffLast.current.at < 400) return
14107
+ diffLast.current = { key: key, at: now }
14108
+ const run = diffSeq.current + 1
14109
+ diffSeq.current = run
13852
14110
  setPatchLoading(true)
13853
14111
  setPatch('')
13854
14112
  try {
13855
14113
  const data = await request('diff', Object.assign({}, base, seed))
14114
+ if (diffSeq.current !== run) return
13856
14115
  setPatch(data.patch)
13857
14116
  } catch (caught) {
14117
+ if (diffSeq.current !== run) return
13858
14118
  setErrorCode(null)
13859
14119
  setError(caught instanceof Error ? caught.message : String(caught))
13860
14120
  } finally {
13861
- setPatchLoading(false)
14121
+ if (diffSeq.current === run) setPatchLoading(false)
13862
14122
  }
13863
14123
  }, [base])
13864
14124
 
@@ -13939,6 +14199,11 @@ window.__ModuleLoader__.load({
13939
14199
  file menu, working-diff shortcut): external first, inline fallback. */
13940
14200
  const showDiff = useCallback((seed) => {
13941
14201
  if (previewDiff(seed) === true) return
14202
+ /* The pane header names the file; its close button clears the selection
14203
+ again, so the inline path owns selectedPath here too. */
14204
+ setSelectedPath(seed.path === undefined ? null : seed.path)
14205
+ /* Compact chrome: the diff lives in a tab, so opening a file shows it. */
14206
+ setSplitTab('diff')
13942
14207
  if (seed.hash !== undefined && seed.hash !== null) {
13943
14208
  setDiffContext({ kind: 'commit', hash: seed.hash, subject: seed.subject || '' })
13944
14209
  void openDiff({ hash: seed.hash, path: seed.path })
@@ -13951,8 +14216,7 @@ window.__ModuleLoader__.load({
13951
14216
  const selectCommit = useCallback(async (commit) => {
13952
14217
  setSelectedHash(commit.hash)
13953
14218
  setSelectedPath(null)
13954
- setPatch('')
13955
- setDiffContext(null)
14219
+ resetDiff()
13956
14220
  setView('detail')
13957
14221
  setDetail(null)
13958
14222
  try {
@@ -13961,7 +14225,7 @@ window.__ModuleLoader__.load({
13961
14225
  setErrorCode(null)
13962
14226
  setError(caught instanceof Error ? caught.message : String(caught))
13963
14227
  }
13964
- }, [base])
14228
+ }, [base, resetDiff])
13965
14229
 
13966
14230
  const checkout = useCallback(async (entry) => {
13967
14231
  if (entry.tag === true) { void run('checkout', { branch: entry.name }); return }
@@ -13988,6 +14252,7 @@ window.__ModuleLoader__.load({
13988
14252
  const openMenuAt = useCallback((event, items) => {
13989
14253
  const element = hostRef.current
13990
14254
  const rect = element === null ? null : element.getBoundingClientRect()
14255
+ setPicker(null)
13991
14256
  setMenu({
13992
14257
  x: event.clientX - (rect === null ? 0 : rect.left),
13993
14258
  y: event.clientY - (rect === null ? 0 : rect.top),
@@ -13997,6 +14262,22 @@ window.__ModuleLoader__.load({
13997
14262
  })
13998
14263
  }, [])
13999
14264
 
14265
+ /* Same anchor math as openMenuAt, but the body is a SearchPicker instead
14266
+ of a fixed item list. Keeping it separate means the generic context
14267
+ menus do not grow search state they never use. */
14268
+ const openPickerAt = useCallback((event, kind) => {
14269
+ const element = hostRef.current
14270
+ const rect = element === null ? null : element.getBoundingClientRect()
14271
+ setMenu(null)
14272
+ setPicker({
14273
+ kind: kind,
14274
+ x: event.clientX - (rect === null ? 0 : rect.left),
14275
+ y: event.clientY - (rect === null ? 0 : rect.top),
14276
+ boundsWidth: rect === null ? 0 : Math.round(rect.width),
14277
+ boundsHeight: rect === null ? 0 : Math.round(rect.height),
14278
+ })
14279
+ }, [])
14280
+
14000
14281
  /* ---------- toast plumbing ---------- */
14001
14282
  /* Declared BEFORE the context menus on purpose: the send-to-chat bindings
14002
14283
  and every menu builder close over pushToast, and a hooks dependency
@@ -14712,7 +14993,13 @@ window.__ModuleLoader__.load({
14712
14993
 
14713
14994
  /* ---------- shared pieces ---------- */
14714
14995
 
14715
- const repoOptions = repoState === null ? [] : repoState.repos
14996
+ /* The scanned row keeps the branch it had when the workspace was
14997
+ listed; after a checkout the branch pill and the tree move on, so the
14998
+ repo label must follow the live HEAD instead of naming the old
14999
+ branch. */
15000
+ const repoOptions = repoState === null ? [] : repoState.repos.map((repo) => (
15001
+ repo.path === repoRoot && branches !== null ? Object.assign({}, repo, { branch: branches.branch }) : repo
15002
+ ))
14716
15003
  /* A linked worktree opened from the worktree view is a repository root
14717
15004
  the workspace scan never listed. Without a header entry the top bar
14718
15005
  kept naming the primary checkout while every branch and change below
@@ -14734,27 +15021,12 @@ window.__ModuleLoader__.load({
14734
15021
  popovers reuse openMenuAt, so they clamp to the panel box and share
14735
15022
  the opaque, frosted menu surface instead of the OS select popup. */
14736
15023
  const openRepoMenu = useCallback((event) => {
14737
- openMenuAt(event, headerRepos.map((repo) => ({
14738
- id: 'repo:' + repo.path,
14739
- icon: 'folder',
14740
- tone: 'accent',
14741
- label: repo.name + (repo.branch === null || repo.branch === undefined ? '' : ' · ' + repo.branch),
14742
- active: repo.path === repoRoot,
14743
- run: () => { if (repo.path !== repoRoot) pickRepo(repo.path) },
14744
- })))
14745
- }, [openMenuAt, headerRepos, repoRoot, pickRepo])
15024
+ openPickerAt(event, 'repo')
15025
+ }, [openPickerAt])
14746
15026
 
14747
15027
  const openBranchSwitchMenu = useCallback((event) => {
14748
- const local = branches === null ? [] : branches.local
14749
- openMenuAt(event, local.map((entry) => ({
14750
- id: 'switch:' + entry.name,
14751
- icon: entry.head === true ? 'star' : 'branch',
14752
- tone: entry.head === true ? 'warn' : 'accent',
14753
- label: entry.name,
14754
- active: entry.head === true,
14755
- run: () => { if (entry.head !== true) void checkout(entry) },
14756
- })))
14757
- }, [openMenuAt, branches, checkout])
15028
+ openPickerAt(event, 'branch')
15029
+ }, [openPickerAt])
14758
15030
 
14759
15031
  const topBar = E('div', { className: 'dig-topbar' },
14760
15032
  E(RepoSelect, {
@@ -14815,7 +15087,7 @@ window.__ModuleLoader__.load({
14815
15087
  const historyPane = view === 'detail'
14816
15088
  ? E(CommitDetail, {
14817
15089
  t: t, detail: detail, selectedPath: selectedPath,
14818
- onBack: () => { setView('history'); setSelectedPath(null); setPatch(''); setDiffContext(null) },
15090
+ onBack: () => { setView('history'); setSelectedPath(null); resetDiff() },
14819
15091
  /* Plain click only highlights the file; the DOUBLE-click opens its
14820
15092
  diff (v0.18.3, one click model across every list). */
14821
15093
  onSelectFile: (file) => { setSelectedPath(file.path) },
@@ -14846,16 +15118,20 @@ window.__ModuleLoader__.load({
14846
15118
  request: request,
14847
15119
  })
14848
15120
 
14849
- const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
15121
+ /* The compact chrome renders the diff INSTEAD of the changes list (the
15122
+ narrow right column has no neighbour to show it beside), so it needs a
15123
+ filling variant; every other chrome keeps the 55%-tall split pane. */
15124
+ const diffPaneOf = (fill) => patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane' + (fill === true ? ' dig-diff-pane-fill' : ''), 'data-pane': 'diff', style: fill === true ? null : paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
14850
15125
  E('div', { className: 'dig-diff-head' },
14851
15126
  E('span', { className: 'dig-mono dig-diff-path' }, selectedPath === null ? '' : selectedPath),
14852
15127
  E('button', {
14853
15128
  type: 'button',
14854
15129
  className: 'dig-icon-btn',
14855
15130
  title: t('toast.close'),
14856
- onClick: () => { setPatch(''); setSelectedPath(null); setDiffContext(null) },
15131
+ onClick: () => { resetDiff(); setSelectedPath(null); setSplitTab('history') },
14857
15132
  }, E(Icon, { name: 'close', size: 12 }))),
14858
15133
  E(DiffBody, { patch: patch, loading: patchLoading, t: t }))
15134
+ const diffPane = diffPaneOf(false)
14859
15135
 
14860
15136
  /* ---------- rail (IDE-style left action strip) ---------- */
14861
15137
 
@@ -15231,7 +15507,7 @@ window.__ModuleLoader__.load({
15231
15507
  else if (spec.id === 'tree') { entry.active = treeOpen; entry.run = () => foldTree(!treeOpen) }
15232
15508
  else if (spec.id === 'worktrees') {
15233
15509
  entry.active = view === 'worktrees'
15234
- entry.run = () => { setPatch(''); setDiffContext(null); setView(view === 'worktrees' ? 'history' : 'worktrees') }
15510
+ entry.run = () => { resetDiff(); setView(view === 'worktrees' ? 'history' : 'worktrees') }
15235
15511
  }
15236
15512
  else if (spec.id === 'float') {
15237
15513
  // Label AND icon follow where the panel actually is: floating panels get
@@ -15377,9 +15653,56 @@ window.__ModuleLoader__.load({
15377
15653
  const worktrees = summary !== null && Array.isArray(summary.worktrees) ? summary.worktrees : []
15378
15654
 
15379
15655
  /* The history↔diff separator only exists while a diff is on screen, and it
15380
- lives inside the middle pane — the diff is the pane below it. */
15656
+ lives inside the middle pane — the diff is the pane below it. Only the
15657
+ three-column chrome keeps that split (v0.26.1). */
15381
15658
  const diffGutter = diffPane === null ? null : gutter('diff', 'next')
15382
15659
 
15660
+ /* v0.26.1 — the stacked chromes (stack and compact) show the history list
15661
+ and the diff in ONE tabbed window below the changes list: clicking a
15662
+ file flips that window to the diff tab instead of splitting the diff out
15663
+ and squeezing the history. `withCount` only matters on compact, whose
15664
+ changes head (and with it the "changes {n}" badge) is hidden; `withTree`
15665
+ adds the tree toggle that chrome has no other way to reach. */
15666
+ const noDiff = patch === '' && patchLoading === false
15667
+ const fillDiff = diffPaneOf(true)
15668
+ const splitTabs = (withCount, withTree) => E('div', { className: 'dig-split-tabs' },
15669
+ withCount === true ? E('span', { className: 'dig-compact-count' }, fill(t('seg.changes'), { n: dirty })) : null,
15670
+ E('button', {
15671
+ type: 'button',
15672
+ className: 'dig-tab',
15673
+ 'data-active': splitTab === 'history' ? 'true' : 'false',
15674
+ 'data-action': 'history-tab',
15675
+ onClick: () => setSplitTab('history'),
15676
+ }, t('seg.history')),
15677
+ E('button', {
15678
+ type: 'button',
15679
+ className: 'dig-tab',
15680
+ 'data-active': splitTab === 'diff' ? 'true' : 'false',
15681
+ 'data-action': 'diff-tab',
15682
+ disabled: noDiff,
15683
+ title: noDiff ? t('diff.empty') : t('diff.codeLabel'),
15684
+ onClick: () => setSplitTab('diff'),
15685
+ }, t('diff.codeLabel')),
15686
+ E('span', { className: 'dig-topbar-spacer' }),
15687
+ withTree === true ? E('button', {
15688
+ type: 'button',
15689
+ className: 'dig-icon-btn' + (treeOpen ? ' dig-icon-btn-active' : ''),
15690
+ 'data-action': 'tree',
15691
+ 'aria-pressed': treeOpen ? 'true' : 'false',
15692
+ title: t('toolbar.tree'),
15693
+ onClick: () => foldTree(!treeOpen),
15694
+ }, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' })) : null)
15695
+
15696
+ /* v0.26.3: a tabbed window keeps BOTH panes mounted and only hides the
15697
+ inactive one. Swapping the elements out unmounted the history list (and
15698
+ the commit detail) on every diff switch, which threw away its scroll
15699
+ position and the windowed list's state. `display:none` keeps the
15700
+ scrollTop (verified in Chromium). The hidden diff measures 0 tall and
15701
+ is dropped by measurePanes' `live > 0` filter, so pane geometry stays
15702
+ honest. */
15703
+ const diffActive = splitTab === 'diff' && fillDiff !== null
15704
+ const tabPane = (active, node) => node === null ? null : E('div', { className: 'dig-tab-pane' + (active === true ? '' : ' dig-tab-pane-hidden') }, node)
15705
+
15383
15706
  let body
15384
15707
  if (repoState === null) {
15385
15708
  /* The scan is still running: a neutral loading state, never the
@@ -15403,31 +15726,19 @@ window.__ModuleLoader__.load({
15403
15726
  onRemove: (tree) => setDialog({ kind: 'worktreeRemove', tree: tree, force: false }),
15404
15727
  }))
15405
15728
  } else if (compact) {
15729
+ /* v0.26.0: the changes list sits on TOP and the lower window is a tab
15730
+ pair (history | diff) instead of a split — opening a file no longer
15731
+ takes the history list away, and the changes list never disappears.
15732
+ The diff tab only exists while there is a patch to show. */
15406
15733
  body = E('div', { className: 'dig-body dig-body-compact', ref: bodyRef },
15407
- E('div', { className: 'dig-compact-bar' },
15408
- E(Segmented, {
15409
- value: view === 'detail' ? 'history' : view,
15410
- onChange: (next) => { setView(next); setPatch(''); setDiffContext(null) },
15411
- items: [
15412
- { id: 'changes', label: fill(t('seg.changes'), { n: dirty }) },
15413
- { id: 'history', label: t('seg.history') },
15414
- ],
15415
- }),
15416
- E('span', { className: 'dig-topbar-spacer' }),
15417
- // Two-way on purpose: treeOpen is persisted, so a header button that
15418
- // only rendered while the tree was OPEN left a folded tree with no way
15419
- // back on this chrome (the rail now carries one too, for the others).
15420
- E('button', {
15421
- type: 'button',
15422
- className: 'dig-icon-btn' + (treeOpen ? ' dig-icon-btn-active' : ''),
15423
- 'data-action': 'tree',
15424
- 'aria-pressed': treeOpen ? 'true' : 'false',
15425
- title: t('toolbar.tree'),
15426
- onClick: () => foldTree(!treeOpen),
15427
- }, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
15428
15734
  treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
15429
15735
  treeOpen ? gutter('tree', 'prev') : null,
15430
- view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
15736
+ E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
15737
+ gutter('changes', 'prev'),
15738
+ E('div', { className: 'dig-pane dig-pane-main' },
15739
+ splitTabs(true, true),
15740
+ tabPane(diffActive === false, historyPane),
15741
+ fillDiff === null ? null : tabPane(diffActive, fillDiff)))
15431
15742
  } else if (columns) {
15432
15743
  body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
15433
15744
  treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
@@ -15441,7 +15752,13 @@ window.__ModuleLoader__.load({
15441
15752
  treeOpen ? gutter('tree', 'prev') : null,
15442
15753
  E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
15443
15754
  gutter('changes', 'prev'),
15444
- E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
15755
+ /* v0.26.1: below the commit box the history list and the diff share
15756
+ one tabbed window — a click on a changed file flips to the diff tab
15757
+ and leaves the list (and its scroll position) alone. */
15758
+ E('div', { className: 'dig-pane dig-pane-main' },
15759
+ splitTabs(false, false),
15760
+ tabPane(diffActive === false, historyPane),
15761
+ fillDiff === null ? null : tabPane(diffActive, fillDiff)))
15445
15762
  }
15446
15763
 
15447
15764
  /* ---------- overlays ---------- */
@@ -15457,6 +15774,37 @@ window.__ModuleLoader__.load({
15457
15774
  onClose: () => setMenu(null),
15458
15775
  }))
15459
15776
  }
15777
+ if (picker !== null) {
15778
+ /* The panel header's repo/branch switchers share the ContextMenu
15779
+ surface (same clamping and outside-click), but their body is the
15780
+ searchable picker instead of an item list. */
15781
+ const branchList = branches === null ? [] : branches.local
15782
+ overlays.push(E(ContextMenu, {
15783
+ key: 'picker',
15784
+ anchor: { x: picker.x, y: picker.y },
15785
+ boundsWidth: picker.boundsWidth,
15786
+ boundsHeight: picker.boundsHeight,
15787
+ items: [],
15788
+ body: E(SearchPicker, {
15789
+ key: picker.kind,
15790
+ mode: picker.kind,
15791
+ t: t,
15792
+ repos: picker.kind === 'repo' ? headerRepos : [],
15793
+ branches: picker.kind === 'branch' ? branchList : [],
15794
+ current: picker.kind === 'repo' ? repoRoot : (branches === null ? '' : branches.branch),
15795
+ onPick: (value, row) => {
15796
+ setPicker(null)
15797
+ if (picker.kind === 'repo') {
15798
+ if (value !== repoRoot) pickRepo(value)
15799
+ return
15800
+ }
15801
+ if (row.head !== true) void checkout(row)
15802
+ },
15803
+ onClose: () => setPicker(null),
15804
+ }),
15805
+ onClose: () => setPicker(null),
15806
+ }))
15807
+ }
15460
15808
  if (railSettings === true) {
15461
15809
  overlays.push(E(RailSettings, {
15462
15810
  key: 'rail-settings',
@@ -15957,7 +16305,21 @@ window.__ModuleLoader__.load({
15957
16305
  '.dig-pane-main{flex:1;min-width:0}',
15958
16306
  '.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-border-l2)}',
15959
16307
  '.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
15960
- '.dig-compact-bar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none}',
16308
+ /* v0.26.1: both stacked chromes show history and diff in one tabbed
16309
+ window; the strip is the window's tab bar (the compact chrome also
16310
+ hosts the changes count and the tree toggle in it). */
16311
+ '.dig-split-tabs{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
16312
+ /* v0.26.3: the tabbed window's bodies stay mounted; the inactive one is
16313
+ hidden so the history list keeps its scroll position and the detail
16314
+ keeps its expanded state. */
16315
+ '.dig-tab-pane{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0}',
16316
+ '.dig-tab-pane-hidden{display:none}',
16317
+ '.dig-compact-count{color:var(--dsw-alias-label-secondary);font-weight:500;font-size:11px;padding:0 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}',
16318
+ '.dig-tab{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;font-weight:500;padding:2px 10px;border-radius:var(--dsw-radius-sm,8px);cursor:pointer;white-space:nowrap;flex:none}',
16319
+ '.dig-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
16320
+ '.dig-tab[data-active="true"]{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary)}',
16321
+ '.dig-tab:disabled{opacity:.45;cursor:default}',
16322
+ '.dig-tab:disabled:hover{background:transparent;color:var(--dsw-alias-label-secondary)}',
15961
16323
  '.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
15962
16324
  // A divider: an 8px hit area (a fingertip needs it) painted as a 1px hairline
15963
16325
  // that lights up on hover, focus or while dragging. touch-action:none is what
@@ -15991,6 +16353,39 @@ window.__ModuleLoader__.load({
15991
16353
  '.dig-picker-name{flex:none;font-weight:600}',
15992
16354
  '.dig-picker-kind{color:var(--dsw-alias-label-tertiary);flex:none}',
15993
16355
  '.dig-picker-path{color:var(--dsw-alias-label-tertiary);margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}',
16356
+ /* v0.27 searchable repo / branch picker (panel header + composer pill).
16357
+ The list scrolls, the search field stays pinned on top.
16358
+ v0.27.1: the composer popover follows dsh-any-background's input
16359
+ opacity (--dsh-any-op-menu-cordis). While that plugin frosts inputs
16360
+ (html.dab-input-frost), a 20% neutral scrim rides on top of the fill
16361
+ so the list still reads as a surface when transparency is turned
16362
+ down. Without the plugin (or with opacity at 0) the base rule alone
16363
+ applies, so the user's setting and the stock opaque menu win. */
16364
+ '.dig-pick{display:flex;flex-direction:column;gap:2px;width:300px;max-width:100%;min-height:0;color:var(--dsw-alias-label-primary)}',
16365
+ '.dig-pick-search{position:sticky;top:0;z-index:1;padding:2px 2px 6px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1))}',
16366
+ '.dig-pick-list{display:flex;flex-direction:column;gap:1px;overflow:auto;min-height:0;max-height:300px;overscroll-behavior:contain}',
16367
+ '.dig-pick-row{display:flex;align-items:center;gap:6px;width:100%;padding:4px 8px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:inherit;font:inherit;font-size:13px;text-align:left;cursor:pointer;min-width:0}',
16368
+ '.dig-pick-row:hover:not(:disabled),.dig-pick-folder:hover{background:var(--dsw-alias-interactive-bg-hover)}',
16369
+ '.dig-pick-row-active{background:var(--dsw-alias-interactive-bg-hover)}',
16370
+ '.dig-pick-row-current{color:var(--dsw-alias-brand-primary)}',
16371
+ '.dig-pick-row-disabled{opacity:.45;cursor:default}',
16372
+ '.dig-pick-row-head{font-weight:600}',
16373
+ '.dig-pick-folder{display:flex;align-items:center;gap:5px;width:100%;padding:3px 8px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;text-align:left;cursor:pointer;min-width:0}',
16374
+ '.dig-pick-chevron{display:inline-flex;flex:none;transition:transform .12s ease;opacity:.6}',
16375
+ '.dig-pick-chevron-open{transform:rotate(90deg)}',
16376
+ '.dig-pick-main{display:flex;flex-direction:column;min-width:0;flex:1}',
16377
+ '.dig-pick-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
16378
+ '.dig-pick-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:12px}',
16379
+ '.dig-pick-icon{display:inline-flex;flex:none}',
16380
+ '.dig-pick-badge{margin-left:auto;flex:none;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 6px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);font-size:10px;line-height:16px}',
16381
+ '.dig-pick-note{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px}',
16382
+ '.dig-pick-count{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}',
16383
+ '.dig-pick-empty{padding:10px 8px;color:var(--dsw-alias-label-tertiary);font-size:12px}',
16384
+ '.dig-cs-menu.dig-cs-picker{width:300px;min-width:260px;max-width:340px;max-height:360px;padding:4px;overflow:hidden;background-color:var(--dsh-any-op-menu-cordis,var(--dsw-alias-bg-layer-2))}',
16385
+ // html.dab-input-frost is dsh-any-background's marker for "input opacity
16386
+ // is on". It only adds our scrim; if the plugin is absent or renames the
16387
+ // marker the menu simply falls back to the transparent fill above.
16388
+ '.dab-input-frost .dig-cs-menu.dig-cs-picker{background-image:linear-gradient(color-mix(in srgb,var(--dsw-alias-label-secondary,#808080) 20%,transparent),color-mix(in srgb,var(--dsw-alias-label-secondary,#808080) 20%,transparent))}',
15994
16389
  /* position:relative: the marquee box paints against this wrapper (the
15995
16390
  scroll container is offset by the search field — same rule as
15996
16391
  .dig-history above). */
@@ -16013,6 +16408,11 @@ window.__ModuleLoader__.load({
16013
16408
  '.dig-folder-head:hover{color:var(--dsw-alias-label-primary)}',
16014
16409
  '.dig-folder-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
16015
16410
  '.dig-count{margin-left:auto;opacity:.6;font-weight:400}',
16411
+ /* v0.26.2: the changes head is the one count that belongs to the WORD, not
16412
+ to the right edge — .dig-count pushes itself to the far right (fine for the
16413
+ branch groups), so the changes header opts back in to sitting right after
16414
+ its title (user report: the number floated in the middle of the row). */
16415
+ '.dig-changes-head .dig-count{margin-left:0}',
16016
16416
  '.dig-chevron{display:inline-flex;transform:rotate(-90deg);transition:transform .12s ease}',
16017
16417
  '.dig-chevron-open{transform:rotate(0deg)}',
16018
16418
  '.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
@@ -16065,7 +16465,10 @@ window.__ModuleLoader__.load({
16065
16465
  filter bar, and painting from the wrong rect was the v0.19.2 drift. */
16066
16466
  '.dig-history{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
16067
16467
  '.dig-history-scroll{flex:1;overflow:auto}',
16068
- '.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 0;cursor:pointer;height:26px;overflow:hidden}',
16468
+ // The row's left edge follows the bars above it (filter row / compact
16469
+ // switch: 4px 6px padding), so the time column and the graph start on the
16470
+ // same inset as the input box instead of flush against the pane edge.
16471
+ '.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 6px;cursor:pointer;height:26px;overflow:hidden}',
16069
16472
  '.dig-commit:hover{background:var(--dsw-alias-interactive-bg-hover)}',
16070
16473
  /* Same theme-aware gray mix as .dig-row-selected (see it). */
16071
16474
  '.dig-commit-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)) 58%,var(--dsw-static-neutral-bluish-75,var(--dsw-alias-bg-layer-3)) 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
@@ -16176,6 +16579,7 @@ window.__ModuleLoader__.load({
16176
16579
  '.dig-detail-files{display:flex;flex-direction:column}',
16177
16580
  '.dig-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
16178
16581
  '.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-border-l2)}',
16582
+ '.dig-diff-pane-fill{flex:1;max-height:none;border-top:none}',
16179
16583
  '.dig-diff-head{display:flex;align-items:center;gap:8px;padding:3px 8px;color:var(--dsw-alias-label-tertiary);flex:none;min-width:0}',
16180
16584
  '.dig-diff-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
16181
16585
  '.dig-diff{flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:calc(12px + var(--dsh-code-font-delta, 0px));line-height:1.5;font-weight:500;min-height:0}',
@@ -17180,50 +17584,48 @@ window.__ModuleLoader__.load({
17180
17584
  behind > 0 ? E('span', { className: 'dig-cs-badge dig-cs-behind', title: t('composer.behindHint') }, '↓' + String(behind)) : null,
17181
17585
  ahead > 0 ? E('span', { className: 'dig-cs-badge dig-cs-ahead', title: t('composer.aheadHint') }, '↑' + String(ahead)) : null,
17182
17586
  error !== '' ? E('span', { className: 'dig-cs-error', title: error }, '×') : null,
17183
- menu === 'repo' ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
17587
+ menu === 'repo' ? E('div', { className: 'dig-cs-menu dig-cs-picker', 'data-cordis-panel': '' },
17184
17588
  /* v0.19.2: one row per checkout — the repository's own name first
17185
17589
  (origin URL basename, falling back to the directory name), and the
17186
17590
  single-level directory name as a small sub line only when the two
17187
17591
  differ (two clones of one repository stay tellable apart). The
17188
- full path lives in the tooltip, not in the row. */
17189
- data.repos.map((row) => {
17190
- const dirName = baseNameOf(row.path)
17191
- const displayName = row.origin !== undefined && row.origin !== '' ? row.origin : dirName
17192
- return E('button', {
17193
- key: row.path,
17194
- type: 'button',
17195
- className: 'dig-cs-item',
17196
- 'data-current': row.path === repoRoot ? 'true' : 'false',
17197
- title: row.path,
17198
- onClick: () => pickRepoRoot(row.path),
17199
- },
17200
- E(Icon, { name: 'commit', size: 11 }),
17201
- E('span', { className: 'dig-cs-item-main' },
17202
- E('span', { className: 'dig-cs-item-name' }, displayName),
17203
- dirName !== displayName ? E('span', { className: 'dig-cs-item-sub' }, dirName) : null))
17592
+ full path lives in the tooltip, not in the row. v0.27 adds the
17593
+ search field so seven clones need no scrolling to find one. */
17594
+ E(SearchPicker, {
17595
+ key: 'composer-repo',
17596
+ mode: 'repo',
17597
+ t: t,
17598
+ repos: data.repos.map((row) => {
17599
+ const dirName = baseNameOf(row.path)
17600
+ const displayName = row.origin !== undefined && row.origin !== '' ? row.origin : dirName
17601
+ return {
17602
+ path: row.path,
17603
+ name: displayName,
17604
+ sub: dirName !== displayName ? dirName : undefined,
17605
+ }
17606
+ }),
17607
+ current: repoRoot,
17608
+ onPick: (path) => pickRepoRoot(path),
17609
+ onClose: () => setMenu(null),
17204
17610
  }))
17205
17611
  : null,
17206
17612
  menu === 'branch' ? (branchLoading === true || branchList === null
17207
17613
  ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
17208
- : E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
17209
- branchList.local.map((entry) => E('button', {
17210
- key: entry.name,
17211
- type: 'button',
17212
- className: 'dig-cs-item',
17213
- 'data-current': entry.head === true ? 'true' : 'false',
17214
- disabled: entry.worktree !== null || checkingOut !== '',
17215
- title: entry.worktree !== null ? t('composer.inWorktree') : (checkingOut !== '' ? t('composer.busy') : entry.name),
17216
- onClick: () => { void checkoutBranch(entry.name) },
17217
- },
17218
- E(Icon, { name: 'branch', size: 11 }),
17219
- E('span', { className: 'dig-cs-item-name' }, entry.name),
17220
- entry.worktree !== null ? E('span', { className: 'dig-cs-item-note' }, t('composer.inWorktree')) : null,
17221
- checkingOut === entry.name ? E('span', { className: 'dig-cs-item-note' }, '…') : null,
17222
- entry.ahead > 0 || entry.behind > 0
17223
- ? E('span', { className: 'dig-cs-item-counts' },
17224
- entry.ahead > 0 ? E('span', { className: 'dig-cs-ahead' }, '↑' + String(entry.ahead)) : null,
17225
- entry.behind > 0 ? E('span', { className: 'dig-cs-behind' }, '↓' + String(entry.behind)) : null)
17226
- : null))))
17614
+ : E('div', { className: 'dig-cs-menu dig-cs-picker', 'data-cordis-panel': '' },
17615
+ E(SearchPicker, {
17616
+ key: 'composer-branch',
17617
+ mode: 'branch',
17618
+ t: t,
17619
+ branches: branchList.local,
17620
+ current: branchName === null ? '' : branchName,
17621
+ busyName: checkingOut,
17622
+ worktreeNote: t('composer.inWorktree'),
17623
+ onPick: (name, entry) => {
17624
+ if (entry.head === true) { setMenu(null); return }
17625
+ void checkoutBranch(name)
17626
+ },
17627
+ onClose: () => setMenu(null),
17628
+ })))
17227
17629
  : null,
17228
17630
  )
17229
17631
  }