dsh-better-workspace 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/README_EN.md +2 -2
- package/dsh.plugin.json +2 -2
- package/package.json +2 -2
- package/src/client.js +363 -4
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
### 添加工作区:官方目录流
|
|
27
27
|
|
|
28
|
-
点「添加工作区」走的是 **DSH 官方的目录拾取流**(v0.12.0 起):webserver 只绑本机 loopback 且有显示会话时,弹出宿主桌面上的**原生选择器**;绑到 `0.0.0.0`(局域网访问)、SSH 转发或宿主没有可用显示会话时,自动切换为官方**应用内目录浏览器**(多栏目录、从根开始的面包屑、可编辑路径、行内新建文件夹、隐藏文件开关)——这正是 0.11.x
|
|
28
|
+
点「添加工作区」走的是 **DSH 官方的目录拾取流**(v0.12.0 起):webserver 只绑本机 loopback 且有显示会话时,弹出宿主桌面上的**原生选择器**;绑到 `0.0.0.0`(局域网访问)、SSH 转发或宿主没有可用显示会话时,自动切换为官方**应用内目录浏览器**(多栏目录、从根开始的面包屑、可编辑路径、行内新建文件夹、隐藏文件开关)——这正是 0.11.x 自绘浏览器打磨的场景,官方补齐后整体让位。选完目录即创建工作区并开始一个新会话,与官方浏览器行为一致;对话空态页的「添加工作区」用的是**同一套官方占用者**,只是入口由本插件渲染(见下)。
|
|
29
29
|
|
|
30
30
|
分组不靠添加时弹窗——**改名即归组**:新建后重命名工作区(名称里的 `/` 就是分组),或直接拖拽进分组行。
|
|
31
31
|
|
|
@@ -72,7 +72,14 @@
|
|
|
72
72
|
- **拖拽还原原版能力**:工作区行可拖拽——拖到另一工作区行上/下半即调整顺序(写回宿主 registry 序),拖到分组行则移动进该分组(改写名称前缀);会话行可拖拽——同组内拖到另一会话上/下即调整顺序,拖到**别组的会话行**会把名字前缀改写到那个组(拖到根层的行 = 移出分组),拖到会话子分组行即移动进该分组,拖到**自己的工作区行**或**树里任何组外的地方**(含空白区域)同样移出分组 —— 拖拽期间工作区行整行高亮、树底部有一条淡虚线提示这个出口;带分组前缀的会话行右键还多一项「移出分组」。显示顺序 = 宿主手动序,拖拽结果立即生效。**磁盘层是文件系统事实,不随拖拽改变**:跨磁盘层的放置只重组名称分组(工作区落回自身磁盘层的末尾),同层放置才走锚点序。拖拽工作区期间,单链合并显示的行(如 工作/A)临时展开回文件夹树——路径里的每一级都是可投放的分组行,松手后自动合并回去。
|
|
73
73
|
- **不丢原生能力**:会话行(打开/重命名/分叉/归档)、每工作区「新会话」(点 + 总是先展开该工作区,让新会话立刻可见)、当前会话高亮、官方状态灯(蓝色运行环 / 琥珀待交互 / 绿色完成提醒)、**状态呼吸灯**(折叠藏起的状态灯向外冒泡:工作区/分组行图标按状态色呼吸发光——琥珀待交互/绿完成,自定义过发光的标题一起呼吸;运行中只在会话行本层蓝色呼吸,不外渗;可在设置关闭)、**活跃定时任务角标**(有活动 Schedule 的会话显示闹钟图标,与官方行一致)、未分组会话兜底、界面文案跟随 DSH 的语言设置(内置 21 种语言,含简繁中文)。
|
|
74
74
|
- **URL/斜杠标题不拆层**:宿主自动生成的会话标题落地时若含 `/`(如复述了 URL),整串自动包上 `“”` 原样显示,绝不解析成层级——只对本次启动后**新建的会话**生效,并等命名稳定(约 20 秒,让原生 AI 命名先落地)才处理;存量会话与你手动命名的 `/` 分组标题永不被改动。引号内(含手动添加)的 `/` 永不切分,孤立引号只当普通字符;无引号但含 URL 的旧标题也有平铺兜底。
|
|
75
|
-
-
|
|
75
|
+
- **新会话页的工作区选择器 = 侧栏那棵树(v0.22.0)**:会话空态页「工作区」按钮的弹层原本是官方的一层平铺标题列表(连磁盘目录嵌套都没有);本插件接管该座位后,弹层绘制与侧栏**同一棵**树——磁盘目录嵌套、「/」名称分组三态、单链合并全部读同一个视图 store,所以侧栏的视图选项一改,**这边立刻跟着变**;当前工作区带勾选标记,分组行可开合(选择器内本地记忆,默认全展开),标题按侧栏的层级叶子显示。底部「添加工作区」仍由**官方目录拾取占用者**承载(原生选择器 / 官方应用内浏览器),不是仿制品。
|
|
76
|
+
|
|
77
|
+
<table>
|
|
78
|
+
<tr>
|
|
79
|
+
<td align="center" width="34%"><img src="docs/screenshots/6-hero-picker.png" alt="新会话页的工作区选择器(侧栏同源的树)"/></td>
|
|
80
|
+
<td valign="top"><b>同一棵树,两个入口</b><br/>左图是「新会话」页工作区按钮的弹层:分组行带 chevron(可开合)、工作区行带文件夹图标与层级缩进、当前工作区带勾选,底部是官方目录流驱动的「添加工作区」。它和左侧栏共用同一份视图状态——在侧栏把「工作区分组」从「磁盘 + 名称」换成「磁盘目录」或「仅名称」,这里的分组结构立即跟着变,不需要刷新,也没有第二份需要同步的数据。</td>
|
|
81
|
+
</tr>
|
|
82
|
+
</table>
|
|
76
83
|
|
|
77
84
|
## 安装
|
|
78
85
|
|
|
@@ -100,7 +107,9 @@ dsh plugin --profile web add dsh-better-workspace # npm 公开包
|
|
|
100
107
|
|
|
101
108
|
DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官方工作区浏览器是它的占用者;本插件以更低优先级注册同一插槽,**整区替换**为层级树版本。数据全部来自插槽标准注入(`useWorkspaces` / `useSessions` 快照钩子),动作(打开会话、重命名/删除工作区、归档等)也走注入的宿主动作——不碰任何私有 API。
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
「添加工作区」完全交给官方:官方组合占用者(原生选择器 / 应用内浏览器)承载全部拾取交互;本插件作为 owner 只负责选完目录后的创建与新会话。**两个 directoryFlow 子洞自 v0.16.0 起都不由本插件声明**——子洞声明是独占的,接管座位的一方永远拿不到 `renderSlot` 授权(相关事故见 CHANGELOG)。侧栏的添加动作因此改为直接驱动官方目录服务(native → 官方 OS 选择器;browse → 提示指向空态页),而**空态页的添加动作直接渲染官方占用者组件本身**:从 slot ledger 读出该洞的占用项(组件 + 它自己的注入面),由本插件补上 owner 对话(open / busy / onPicked / onCancel / onError)。原生与 browse 宿主各自保留出厂交互;ledger 读不到占用项时,少的只是「添加工作区」这一项,选择器照常工作。该机制 0.1.2+ 宿主即可用,升级先后都收敛。
|
|
111
|
+
|
|
112
|
+
会话空态页的工作区座位(`conversation.hero.workspace`)与侧栏同理:本插件以更低优先级注册同一座位,弹层换成侧栏同源的树;该座位**不声明任何子洞**,所以官方 picker entry 的所有权不受影响。
|
|
104
113
|
|
|
105
114
|
视图状态(分组折叠、会话展开)、外观自定义与插件设置都通过 dsh 客户端 store 持久化在浏览器本地(`dsh.betterWorkspace.view.v1`)。注意:该 store 的 hydration 是整值替换、不合并默认值——升级后如果遇到状态字段缺失,重启前的旧状态按旧结构保留,缺的键自动回退默认。
|
|
106
115
|
|
|
@@ -118,7 +127,8 @@ DSH 的侧边栏浏览区是一个公开插槽 `sidebar.workspaces`(single),官
|
|
|
118
127
|
|
|
119
128
|
- 会话**跨工作区**拖拽仍被拒绝(会话归属工作区);同一工作区内的分组归属已随拖拽生效(v0.18.0:拖到别组的行改写前缀,拖到自己的工作区行移出分组,右键也有「移出分组」)。
|
|
120
129
|
- 搜索为本地标题过滤(两层扁平化后逐工作区匹配);宿主内容搜索(`session.search`)计划接回。
|
|
121
|
-
- flat(平铺)
|
|
130
|
+
- flat(平铺)视图未接管,层级树即视图。侧栏切到「单列表 / 按工作区」时,新会话页的选择器退化为**一层完整标题**的工作区列表(仍可选,不再分组)。
|
|
131
|
+
- 新会话页选择器里的分组开合是**选择器本地状态**(默认全展开):它不写侧栏的折叠状态,免得选工作区时被侧栏的折叠挡住目标。
|
|
122
132
|
|
|
123
133
|
## 开发
|
|
124
134
|
|
package/README_EN.md
CHANGED
|
@@ -25,7 +25,7 @@ disk layer (paths, automatic) name layer (titles, virtual)
|
|
|
25
25
|
|
|
26
26
|
### Add workspace: the official directory flow
|
|
27
27
|
|
|
28
|
-
"Add workspace" runs the **official DSH picking flow** (since v0.12.0): a loopback-only webserver bind with a display session opens the host's **native OS chooser**; an all-interfaces/LAN bind, an SSH launch, or a headless host switches to the official **in-app directory browser** (Miller columns, root-anchored breadcrumbs, editable path, inline new-folder row, hidden-file toggle) — the exact scenario this plugin's hand-rolled browser polished until the official one landed. Picking a directory creates the workspace and starts a new session in it, exactly like the shipped browser; the conversation empty-state
|
|
28
|
+
"Add workspace" runs the **official DSH picking flow** (since v0.12.0): a loopback-only webserver bind with a display session opens the host's **native OS chooser**; an all-interfaces/LAN bind, an SSH launch, or a headless host switches to the official **in-app directory browser** (Miller columns, root-anchored breadcrumbs, editable path, inline new-folder row, hidden-file toggle) — the exact scenario this plugin's hand-rolled browser polished until the official one landed. Picking a directory creates the workspace and starts a new session in it, exactly like the shipped browser; the conversation empty-state picker drives that same official flow from this plugin's tree popover (v0.22.0).
|
|
29
29
|
|
|
30
30
|
Grouping happens by naming, not by a popup: rename the workspace after creation (every `/` in the title is a group) or drag it onto a folder row.
|
|
31
31
|
|
|
@@ -72,7 +72,7 @@ Session titles nest on `/` too (e.g. `test1/plugin maintenance`). Session groups
|
|
|
72
72
|
- **Native drag restored**: drag a workspace row above/below another to reorder (writes back the host registry order) or onto a folder row to move it into that group (rewrites the title prefix); drag session rows to reorder inside their group, onto a row of ANOTHER group to move into that group (onto a root-level row to leave the group), onto a session-group row to move into it, or onto their OWN workspace row / anywhere outside a group (empty space included) to leave it — the workspace row highlights and the tree shows a faint dashed hint while such a drag is in flight; a grouped session row also offers "Move out of group" in its context menu. Display order = host manual order, drag results are visible immediately. **The disk layer is a filesystem fact and never changes through dragging**: a cross-level drop only re-groups the name prefix (the workspace lands at the end of its own disk level), same-level drops take the anchor order. While dragging a workspace, merged single-chain rows (e.g. work/A) temporarily re-expand into folder rows — every level of the path becomes a drop target, and chains merge back when the drag ends.
|
|
73
73
|
- **Native capabilities kept**: open/rename/fork/archive sessions, per-workspace new-session (the + button always expands the workspace first so the new session is immediately visible), current-session highlight, official status dots (blue running ring / amber pending / green done), **status breathing lights** (dots hidden by collapse bubble outward: workspace/folder rows breathe on their icon in the status color — amber pending / green done, custom-glow labels breathe along; running breathes blue only on the session row itself and never relays; off-able in settings), **active-schedule badge** (alarm clock icon for sessions with active Schedule records, matching the official rows), ungrouped-session fallback, UI copy in 21 languages following the DSH language preference (Simplified and Traditional Chinese included).
|
|
74
74
|
- **Slash-bearing titles never nest from URLs**: when a host-generated session title lands containing `/` (e.g. echoing a pasted URL), it is auto-wrapped in `“”` and shown verbatim — only for sessions **created after this launch**, and only once the name stays stable for ~20s (letting the native AI naming land first); pre-existing sessions and your manually named `/` groupings are never touched. Quotes (manual ones too) never split on `/`, and a lone quote is just an ordinary character; unquoted URL tails still fall back flat.
|
|
75
|
-
- **
|
|
75
|
+
- **The New Session workspace picker IS the sidebar tree (v0.22.0)**: the empty-state "Workspace" chip used to open the shipped one-level list of flat titles (no disk nesting at all). This plugin shadows that seat (lowest priority renders) and draws the **same** tree the sidebar draws — disk nesting, the tri-state "/" name layer, chain compression — all from the one view store, so changing a view option on the sidebar moves this popover immediately. The current workspace carries a check, folder rows fold (picker-local, fully expanded by default), and the pinned "Add workspace" action still renders the **shipped directory-picking occupant** (native chooser / official in-app browser) instead of a copy.
|
|
76
76
|
|
|
77
77
|
## Install
|
|
78
78
|
|
package/dsh.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-better-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
|
-
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启)
|
|
5
|
+
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树。Workspace tree for the DSH sidebar workspace list, nested by official disk folders with optional \"/\"-name grouping; the New Session workspace picker shows the same tree.",
|
|
6
6
|
"engines": {
|
|
7
7
|
"dsh": ">=0.0.1"
|
|
8
8
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-workspace",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default); add-workspace rides the official directory flow.",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "侧边栏工作区树:按官方磁盘目录嵌套(官方 workspace-tree 语义),可选按名称中的 / 分组(默认关闭,设置里开启);新会话页的工作区选择器同树(同一份视图选项即时同步);添加工作区走官方目录流。Workspace tree for the DSH sidebar: disk-path nesting (official workspace-tree semantics), with optional \"/\"-name grouping (off by default), and the same tree in the New Session workspace picker; add-workspace rides the official directory flow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/client.js
CHANGED
|
@@ -139,6 +139,8 @@ window.__ModuleLoader__.load({
|
|
|
139
139
|
'flow.creating': '正在创建…',
|
|
140
140
|
'add.guide.title': "应用内添加工作区",
|
|
141
141
|
'add.guide': "此主机不提供桌面目录选择器。请到「新会话」页,用官方的「添加工作区」打开应用内浏览器。",
|
|
142
|
+
'heroPicker.error.title': "无法添加工作区",
|
|
143
|
+
'heroPicker.retry': "重试",
|
|
142
144
|
'error.title': '出错了',
|
|
143
145
|
'cancel': '取消',
|
|
144
146
|
'create': '创建',
|
|
@@ -280,6 +282,8 @@ window.__ModuleLoader__.load({
|
|
|
280
282
|
'flow.creating': 'Creating…',
|
|
281
283
|
'add.guide.title': "Add workspace in-app",
|
|
282
284
|
'add.guide': "This host has no desktop directory chooser. Use the official \"Add workspace\" entry on the New Session page.",
|
|
285
|
+
'heroPicker.error.title': "Could not add workspace",
|
|
286
|
+
'heroPicker.retry': "Retry",
|
|
283
287
|
'error.title': 'Something went wrong',
|
|
284
288
|
'cancel': 'Cancel',
|
|
285
289
|
'create': 'Create',
|
|
@@ -429,6 +433,8 @@ window.__ModuleLoader__.load({
|
|
|
429
433
|
'flow.creating': 'جارٍ الإنشاء…',
|
|
430
434
|
'add.guide.title': "إضافة مساحة عمل داخل التطبيق",
|
|
431
435
|
'add.guide': "لا يوفر هذا المضيف منتقي مجلدات سطح المكتب. استخدم إدخال \"إضافة مساحة عمل\" الرسمي في صفحة الجلسة الجديدة.",
|
|
436
|
+
'heroPicker.error.title': "تعذر إضافة مساحة العمل",
|
|
437
|
+
'heroPicker.retry': "إعادة المحاولة",
|
|
432
438
|
'error.title': 'حدث خطأ ما',
|
|
433
439
|
'cancel': 'إلغاء',
|
|
434
440
|
'create': 'إنشاء',
|
|
@@ -570,6 +576,8 @@ window.__ModuleLoader__.load({
|
|
|
570
576
|
'flow.creating': 'Wird erstellt…',
|
|
571
577
|
'add.guide.title': "Arbeitsbereich in der App hinzufügen",
|
|
572
578
|
'add.guide': "Dieser Host bietet keinen Desktop-Ordnerdialog. Nutze den offiziellen Eintrag „Arbeitsbereich hinzufügen“ auf der Seite „Neue Sitzung“.",
|
|
579
|
+
'heroPicker.error.title': "Arbeitsbereich konnte nicht hinzugefügt werden",
|
|
580
|
+
'heroPicker.retry': "Erneut versuchen",
|
|
573
581
|
'error.title': 'Etwas ist schiefgelaufen',
|
|
574
582
|
'cancel': 'Abbrechen',
|
|
575
583
|
'create': 'Erstellen',
|
|
@@ -711,6 +719,8 @@ window.__ModuleLoader__.load({
|
|
|
711
719
|
'flow.creating': 'Création…',
|
|
712
720
|
'add.guide.title': "Ajouter un espace de travail dans l’application",
|
|
713
721
|
'add.guide': "Cet hôte n’a pas de sélecteur de dossier natif. Utilisez l’entrée officielle « Ajouter un espace de travail » sur la page Nouvelle session.",
|
|
722
|
+
'heroPicker.error.title': "Impossible d'ajouter l'espace de travail",
|
|
723
|
+
'heroPicker.retry': "Réessayer",
|
|
714
724
|
'error.title': 'Une erreur est survenue',
|
|
715
725
|
'cancel': 'Annuler',
|
|
716
726
|
'create': 'Créer',
|
|
@@ -852,6 +862,8 @@ window.__ModuleLoader__.load({
|
|
|
852
862
|
'flow.creating': 'बनाया जा रहा है…',
|
|
853
863
|
'add.guide.title': "ऐप में कार्यस्थान जोड़ें",
|
|
854
864
|
'add.guide': "इस होस्ट में डेस्कटॉप फ़ोल्डर चयनकर्ता नहीं है। नए सत्र पृष्ठ पर आधिकारिक \"कार्यस्थान जोड़ें\" प्रविष्टि का उपयोग करें।",
|
|
865
|
+
'heroPicker.error.title': "कार्यस्थान जोड़ा नहीं जा सका",
|
|
866
|
+
'heroPicker.retry': "पुनः प्रयास करें",
|
|
855
867
|
'error.title': 'कुछ गड़बड़ हो गई',
|
|
856
868
|
'cancel': 'रद्द करें',
|
|
857
869
|
'create': 'बनाएँ',
|
|
@@ -993,6 +1005,8 @@ window.__ModuleLoader__.load({
|
|
|
993
1005
|
'flow.creating': 'Membuat…',
|
|
994
1006
|
'add.guide.title': "Tambah ruang kerja di aplikasi",
|
|
995
1007
|
'add.guide': "Host ini tidak punya pemilih folder desktop. Gunakan entri resmi \"Tambah ruang kerja\" di halaman Sesi Baru.",
|
|
1008
|
+
'heroPicker.error.title': "Tidak dapat menambahkan ruang kerja",
|
|
1009
|
+
'heroPicker.retry': "Coba lagi",
|
|
996
1010
|
'error.title': 'Terjadi kesalahan',
|
|
997
1011
|
'cancel': 'Batal',
|
|
998
1012
|
'create': 'Buat',
|
|
@@ -1134,6 +1148,8 @@ window.__ModuleLoader__.load({
|
|
|
1134
1148
|
'flow.creating': 'Creazione…',
|
|
1135
1149
|
'add.guide.title': "Aggiungi area di lavoro nell’app",
|
|
1136
1150
|
'add.guide': "Questo host non ha un selettore di cartelle desktop. Usa la voce ufficiale «Aggiungi area di lavoro» nella pagina Nuova sessione.",
|
|
1151
|
+
'heroPicker.error.title': "Impossibile aggiungere l'area di lavoro",
|
|
1152
|
+
'heroPicker.retry': "Riprova",
|
|
1137
1153
|
'error.title': 'Si è verificato un errore',
|
|
1138
1154
|
'cancel': 'Annulla',
|
|
1139
1155
|
'create': 'Crea',
|
|
@@ -1275,6 +1291,8 @@ window.__ModuleLoader__.load({
|
|
|
1275
1291
|
'flow.creating': '作成中…',
|
|
1276
1292
|
'add.guide.title': "アプリ内でワークスペースを追加",
|
|
1277
1293
|
'add.guide': "このホストにはデスクトップのフォルダー選択画面がありません。「新規セッション」ページの公式の「ワークスペースを追加」から開いてください。",
|
|
1294
|
+
'heroPicker.error.title': "ワークスペースを追加できません",
|
|
1295
|
+
'heroPicker.retry': "再試行",
|
|
1278
1296
|
'error.title': 'エラーが発生しました',
|
|
1279
1297
|
'cancel': 'キャンセル',
|
|
1280
1298
|
'create': '作成',
|
|
@@ -1416,6 +1434,8 @@ window.__ModuleLoader__.load({
|
|
|
1416
1434
|
'flow.creating': '만드는 중…',
|
|
1417
1435
|
'add.guide.title': "앱에서 워크스페이스 추가",
|
|
1418
1436
|
'add.guide': "이 호스트에는 데스크톱 폴더 선택기가 없습니다. 새 세션 페이지의 공식 \"워크스페이스 추가\" 항목을 사용하세요.",
|
|
1437
|
+
'heroPicker.error.title': "워크스페이스를 추가할 수 없습니다",
|
|
1438
|
+
'heroPicker.retry': "다시 시도",
|
|
1419
1439
|
'error.title': '문제가 발생했습니다',
|
|
1420
1440
|
'cancel': '취소',
|
|
1421
1441
|
'create': '만들기',
|
|
@@ -1557,6 +1577,8 @@ window.__ModuleLoader__.load({
|
|
|
1557
1577
|
'flow.creating': 'Bezig met aanmaken…',
|
|
1558
1578
|
'add.guide.title': "Werkruimte in de app toevoegen",
|
|
1559
1579
|
'add.guide': "Deze host heeft geen systeemmapkiezer. Gebruik de officiële optie \"Werkruimte toevoegen\" op de pagina Nieuwe sessie.",
|
|
1580
|
+
'heroPicker.error.title': "Kan werkruimte niet toevoegen",
|
|
1581
|
+
'heroPicker.retry': "Opnieuw proberen",
|
|
1560
1582
|
'error.title': 'Er is iets misgegaan',
|
|
1561
1583
|
'cancel': 'Annuleren',
|
|
1562
1584
|
'create': 'Aanmaken',
|
|
@@ -1698,6 +1720,8 @@ window.__ModuleLoader__.load({
|
|
|
1698
1720
|
'flow.creating': 'Tworzenie…',
|
|
1699
1721
|
'add.guide.title': "Dodaj obszar roboczy w aplikacji",
|
|
1700
1722
|
'add.guide': "Ten host nie ma systemowego wyboru folderów. Użyj oficjalnej pozycji „Dodaj obszar roboczy” na stronie Nowa sesja.",
|
|
1723
|
+
'heroPicker.error.title': "Nie można dodać obszaru roboczego",
|
|
1724
|
+
'heroPicker.retry': "Ponów",
|
|
1701
1725
|
'error.title': 'Coś poszło nie tak',
|
|
1702
1726
|
'cancel': 'Anuluj',
|
|
1703
1727
|
'create': 'Utwórz',
|
|
@@ -1839,6 +1863,8 @@ window.__ModuleLoader__.load({
|
|
|
1839
1863
|
'flow.creating': 'Criando…',
|
|
1840
1864
|
'add.guide.title': "Adicionar área de trabalho no app",
|
|
1841
1865
|
'add.guide': "Este host não tem seletor de pastas do sistema. Use a entrada oficial \"Adicionar área de trabalho\" na página Nova sessão.",
|
|
1866
|
+
'heroPicker.error.title': "Não foi possível adicionar o espaço de trabalho",
|
|
1867
|
+
'heroPicker.retry': "Tentar novamente",
|
|
1842
1868
|
'error.title': 'Algo deu errado',
|
|
1843
1869
|
'cancel': 'Cancelar',
|
|
1844
1870
|
'create': 'Criar',
|
|
@@ -1980,6 +2006,8 @@ window.__ModuleLoader__.load({
|
|
|
1980
2006
|
'flow.creating': 'Создание…',
|
|
1981
2007
|
'add.guide.title': "Добавить рабочую область в приложении",
|
|
1982
2008
|
'add.guide': "На этом хосте нет системного выбора папок. Используйте официальный пункт «Добавить рабочую область» на странице «Новый сеанс».",
|
|
2009
|
+
'heroPicker.error.title': "Не удалось добавить рабочую область",
|
|
2010
|
+
'heroPicker.retry': "Повторить",
|
|
1983
2011
|
'error.title': 'Что-то пошло не так',
|
|
1984
2012
|
'cancel': 'Отмена',
|
|
1985
2013
|
'create': 'Создать',
|
|
@@ -2121,6 +2149,8 @@ window.__ModuleLoader__.load({
|
|
|
2121
2149
|
'flow.creating': 'Skapar…',
|
|
2122
2150
|
'add.guide.title': "Lägg till arbetsyta i appen",
|
|
2123
2151
|
'add.guide': "Den här värden har ingen systemmappväljare. Använd den officiella posten \"Lägg till arbetsyta\" på sidan Ny session.",
|
|
2152
|
+
'heroPicker.error.title': "Kunde inte lägga till arbetsytan",
|
|
2153
|
+
'heroPicker.retry': "Försök igen",
|
|
2124
2154
|
'error.title': 'Något gick fel',
|
|
2125
2155
|
'cancel': 'Avbryt',
|
|
2126
2156
|
'create': 'Skapa',
|
|
@@ -2262,6 +2292,8 @@ window.__ModuleLoader__.load({
|
|
|
2262
2292
|
'flow.creating': 'กำลังสร้าง…',
|
|
2263
2293
|
'add.guide.title': "เพิ่มเวิร์กสเปซในแอป",
|
|
2264
2294
|
'add.guide': "โฮสต์นี้ไม่มีตัวเลือกโฟลเดอร์ของระบบ ใช้รายการ \"เพิ่มเวิร์กสเปซ\" อย่างเป็นทางการในหน้าเซสชันใหม่",
|
|
2295
|
+
'heroPicker.error.title': "ไม่สามารถเพิ่มพื้นที่ทำงานได้",
|
|
2296
|
+
'heroPicker.retry': "ลองอีกครั้ง",
|
|
2265
2297
|
'error.title': 'เกิดข้อผิดพลาด',
|
|
2266
2298
|
'cancel': 'ยกเลิก',
|
|
2267
2299
|
'create': 'สร้าง',
|
|
@@ -2403,6 +2435,8 @@ window.__ModuleLoader__.load({
|
|
|
2403
2435
|
'flow.creating': 'Oluşturuluyor…',
|
|
2404
2436
|
'add.guide.title': "Uygulama içinde çalışma alanı ekle",
|
|
2405
2437
|
'add.guide': "Bu ana makinede masaüstü klasör seçici yok. Yeni Oturum sayfasındaki resmî \"Çalışma alanı ekle\" girişini kullanın.",
|
|
2438
|
+
'heroPicker.error.title': "Çalışma alanı eklenemedi",
|
|
2439
|
+
'heroPicker.retry': "Yeniden dene",
|
|
2406
2440
|
'error.title': 'Bir şeyler ters gitti',
|
|
2407
2441
|
'cancel': 'İptal',
|
|
2408
2442
|
'create': 'Oluştur',
|
|
@@ -2544,6 +2578,8 @@ window.__ModuleLoader__.load({
|
|
|
2544
2578
|
'flow.creating': 'Đang tạo…',
|
|
2545
2579
|
'add.guide.title': "Thêm không gian làm việc trong ứng dụng",
|
|
2546
2580
|
'add.guide': "Máy chủ này không có trình chọn thư mục trên màn hình. Dùng mục chính thức \"Thêm không gian làm việc\" ở trang Phiên mới.",
|
|
2581
|
+
'heroPicker.error.title': "Không thể thêm không gian làm việc",
|
|
2582
|
+
'heroPicker.retry': "Thử lại",
|
|
2547
2583
|
'error.title': 'Đã xảy ra lỗi',
|
|
2548
2584
|
'cancel': 'Hủy',
|
|
2549
2585
|
'create': 'Tạo',
|
|
@@ -2685,6 +2721,8 @@ window.__ModuleLoader__.load({
|
|
|
2685
2721
|
'flow.creating': '建立緊…',
|
|
2686
2722
|
'add.guide.title': "應用程式內新增工作區",
|
|
2687
2723
|
'add.guide': "呢部主機冇桌面資料夾選擇器。請去「新工作階段」頁,用官方嘅「新增工作區」開應用程式內瀏覽器。",
|
|
2724
|
+
'heroPicker.error.title': "無法新增工作區",
|
|
2725
|
+
'heroPicker.retry': "重試",
|
|
2688
2726
|
'error.title': '出咗錯',
|
|
2689
2727
|
'cancel': '取消',
|
|
2690
2728
|
'create': '建立',
|
|
@@ -2826,6 +2864,8 @@ window.__ModuleLoader__.load({
|
|
|
2826
2864
|
'flow.creating': '建立緊…',
|
|
2827
2865
|
'add.guide.title': "應用程式內新增工作區",
|
|
2828
2866
|
'add.guide': "呢部主機冇桌面資料夾選擇器。請去「新工作階段」頁,用官方嘅「新增工作區」開應用程式內瀏覽器。",
|
|
2867
|
+
'heroPicker.error.title': "無法新增工作區",
|
|
2868
|
+
'heroPicker.retry': "重試",
|
|
2829
2869
|
'error.title': '出咗錯',
|
|
2830
2870
|
'cancel': '取消',
|
|
2831
2871
|
'create': '建立',
|
|
@@ -2967,6 +3007,8 @@ window.__ModuleLoader__.load({
|
|
|
2967
3007
|
'flow.creating': '正在建立…',
|
|
2968
3008
|
'add.guide.title': "應用程式內新增工作區",
|
|
2969
3009
|
'add.guide': "此主機沒有桌面資料夾選擇器。請至「新工作階段」頁,用官方的「新增工作區」開啟應用程式內瀏覽器。",
|
|
3010
|
+
'heroPicker.error.title': "無法新增工作區",
|
|
3011
|
+
'heroPicker.retry': "重試",
|
|
2970
3012
|
'error.title': '發生錯誤',
|
|
2971
3013
|
'cancel': '取消',
|
|
2972
3014
|
'create': '建立',
|
|
@@ -3657,6 +3699,20 @@ window.__ModuleLoader__.load({
|
|
|
3657
3699
|
'.bw-row-icon{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
|
|
3658
3700
|
'.bw-chevron{flex:none;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#9a9a9a);transition:transform .15s ease}',
|
|
3659
3701
|
'.bw-chevron-open{transform:rotate(90deg)}',
|
|
3702
|
+
// Hero picker rows (v0.22.0): hierarchy rides the row itself — an
|
|
3703
|
+
// indentation gutter, a folder chevron or workspace glyph, and the
|
|
3704
|
+
// trailing check. The row mirrors the primitives menu cell (same hover
|
|
3705
|
+
// and focus fill) because the fixed 14px icon cell of that row crops any
|
|
3706
|
+
// indentation nested inside it.
|
|
3707
|
+
'.bw-pick-row{display:flex;align-items:center;gap:6px;width:100%;min-height:30px;padding:4px 8px;border:none;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:13px;cursor:pointer;text-align:left}',
|
|
3708
|
+
'.bw-pick-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.15))}',
|
|
3709
|
+
'.bw-pick-row:focus-visible{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.15));outline:none}',
|
|
3710
|
+
'.bw-pick-indent{flex:none}',
|
|
3711
|
+
'.bw-pick-glyph{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dsw-alias-label-tertiary,#9a9a9a)}',
|
|
3712
|
+
'.bw-pick-glyph svg{width:16px;height:16px}',
|
|
3713
|
+
'.bw-pick-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
3714
|
+
'.bw-pick-folder{font-weight:600}',
|
|
3715
|
+
'.bw-pick-check{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-brand-primary,#5b8def)}',
|
|
3660
3716
|
// overflow:hidden (needed for the ellipsis) clips the outline: 3px of
|
|
3661
3717
|
// padding covers the widest rim (2px at the top of the slider). margin
|
|
3662
3718
|
// cancels it, so text still starts and truncates where it did.
|
|
@@ -5138,7 +5194,7 @@ window.__ModuleLoader__.load({
|
|
|
5138
5194
|
* solid icon) clears the row's entry instead of pinning a redundant copy;
|
|
5139
5195
|
* Reset removes the entry outright.
|
|
5140
5196
|
*/
|
|
5141
|
-
function CustomizeDialog({ open, initial, defaults, kind, onChange, onReset, onClose, t }) {
|
|
5197
|
+
function CustomizeDialog({ open, seedKey, initial, defaults, kind, onChange, onReset, onClose, t }) {
|
|
5142
5198
|
// Icons render only for workspace / workspace-folder rows. Session rows
|
|
5143
5199
|
// already carry the official status dot (pending/running/done) in the
|
|
5144
5200
|
// leading slot, and session sub-group rows have no icon either — so the
|
|
@@ -5146,11 +5202,20 @@ window.__ModuleLoader__.load({
|
|
|
5146
5202
|
const allowIcon = kind === 'folder' || kind === 'workspace'
|
|
5147
5203
|
const fallback = readAppearance(defaults)
|
|
5148
5204
|
const [draft, setDraft] = React.useState(DEFAULT_APPEARANCE)
|
|
5205
|
+
// The seed reads the CURRENT initial through a ref, and the effect keys
|
|
5206
|
+
// on the edited row (seedKey) instead of the initial object: the owner
|
|
5207
|
+
// rebuilds that object on every render, and a running Session re-renders
|
|
5208
|
+
// the browser every few seconds — keying on the object re-seeded the
|
|
5209
|
+
// draft mid-edit and threw the operator's picks away (the "flash back to
|
|
5210
|
+
// default" report). Only opening the dialog or switching rows re-seeds.
|
|
5211
|
+
const initialRef = React.useRef(initial)
|
|
5212
|
+
initialRef.current = initial
|
|
5149
5213
|
React.useEffect(() => {
|
|
5150
5214
|
if (!open) return
|
|
5151
|
-
const
|
|
5152
|
-
|
|
5153
|
-
|
|
5215
|
+
const seed = initialRef.current
|
|
5216
|
+
const base = readAppearance(seed)
|
|
5217
|
+
setDraft({ ...base, icon: (seed && seed.icon) || 'solid' })
|
|
5218
|
+
}, [open, seedKey])
|
|
5154
5219
|
if (!open) return null
|
|
5155
5220
|
const unchanged = APPEARANCE_FIELDS.every((field) => draft[field] === fallback[field])
|
|
5156
5221
|
&& (!allowIcon || (draft.icon || 'solid') === 'solid')
|
|
@@ -5417,6 +5482,256 @@ window.__ModuleLoader__.load({
|
|
|
5417
5482
|
)
|
|
5418
5483
|
}
|
|
5419
5484
|
|
|
5485
|
+
/* ====================== hero workspace picker ===================== */
|
|
5486
|
+
|
|
5487
|
+
/** The conversation empty-state picker's directory-flow hole (declared and owned by the shipped picker entry). */
|
|
5488
|
+
const HERO_FLOW_HOLE = 'conversation.hero.workspace.directoryFlow'
|
|
5489
|
+
/** Menu id of the picker's pinned add action — namespaced so it can never collide with a workspace id. */
|
|
5490
|
+
const HERO_ADD_ID = '::bw-hero-add'
|
|
5491
|
+
/** Stable empty array for memo dependencies. */
|
|
5492
|
+
const EMPTY_LIST = []
|
|
5493
|
+
|
|
5494
|
+
/**
|
|
5495
|
+
* One picker row. The row is drawn here rather than through the primitives
|
|
5496
|
+
* MenuItemButton because the tree needs its own layout — an indentation
|
|
5497
|
+
* gutter, the folder chevron and the trailing check — while the fixed
|
|
5498
|
+
* 14px icon cell of that row crops any indentation placed inside it. The
|
|
5499
|
+
* list chrome (card, scroll, outside click, arrow walk, focus return) is
|
|
5500
|
+
* still the primitives Menu's: its keyboard walk reads
|
|
5501
|
+
* `button[role="menuitem"]`, which is exactly what this renders.
|
|
5502
|
+
*/
|
|
5503
|
+
function PickerRow({ depth, glyph, label, folder, checked, onSelect }) {
|
|
5504
|
+
return E('button', {
|
|
5505
|
+
type: 'button',
|
|
5506
|
+
role: 'menuitem',
|
|
5507
|
+
className: 'bw-pick-row',
|
|
5508
|
+
onClick: onSelect,
|
|
5509
|
+
},
|
|
5510
|
+
E('span', { className: 'bw-pick-indent', style: { width: (depth * 14) + 'px' } }),
|
|
5511
|
+
E('span', { className: 'bw-pick-glyph' }, glyph),
|
|
5512
|
+
E('span', { className: cls('bw-pick-name', folder && 'bw-pick-folder') }, label),
|
|
5513
|
+
checked ? E('span', { className: 'bw-pick-check' }, icon('IconCheckOutlineRegular', 14)) : null,
|
|
5514
|
+
)
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
/**
|
|
5518
|
+
* Conversation empty-state workspace picker (v0.22.0).
|
|
5519
|
+
*
|
|
5520
|
+
* The shipped picker lists every workspace title flat, in one level, with
|
|
5521
|
+
* no disk nesting at all; this seat shadows it (priority -1 — lowest
|
|
5522
|
+
* renders) and draws the SAME store-backed tree the sidebar draws: disk
|
|
5523
|
+
* nesting by the official owning-parent semantics, the tri-state "/" name
|
|
5524
|
+
* layer and chain compression all read the one view store, so a view
|
|
5525
|
+
* option changed on either surface moves both. The chip, its anchor and
|
|
5526
|
+
* the pick semantics stay with ui-conversation — this component renders the
|
|
5527
|
+
* popover only and reports a pick through the owner's onPick.
|
|
5528
|
+
*
|
|
5529
|
+
* Adding a workspace stays official. Shadowing the seat forfeits the
|
|
5530
|
+
* child-hole render authorization (a children declaration is exclusive,
|
|
5531
|
+
* and the shipped entry keeps owning HERO_FLOW_HOLE), so the shipped
|
|
5532
|
+
* occupant COMPONENT is read off the slot ledger and rendered directly —
|
|
5533
|
+
* with this surface supplying the same owner conversation the shipped
|
|
5534
|
+
* picker would, plus that entry's own injected face. Native and browse
|
|
5535
|
+
* hosts therefore keep the interaction they shipped with; a host whose
|
|
5536
|
+
* ledger hides the entry loses the add action, never the picker.
|
|
5537
|
+
*/
|
|
5538
|
+
function HeroWorkspacePicker(props) {
|
|
5539
|
+
const t = typeof props.t === 'function' ? props.t : (key => key)
|
|
5540
|
+
const useWorkspaces = props.useWorkspaces
|
|
5541
|
+
const useStore = props.useStore
|
|
5542
|
+
const useDirectoryFlow = props.useDirectoryFlow
|
|
5543
|
+
// Hook order must not depend on the injection face arriving: the fallback
|
|
5544
|
+
// is a plain function (not a hook), so both branches call the same way.
|
|
5545
|
+
const useFlowOccupancy = typeof useDirectoryFlow === 'function' ? useDirectoryFlow : () => 0
|
|
5546
|
+
const anchorRef = props.anchorRef
|
|
5547
|
+
const selectedId = props.selectedId
|
|
5548
|
+
const onPick = props.onPick
|
|
5549
|
+
const onClose = props.onClose
|
|
5550
|
+
const heroFlowEntry = props.heroFlowEntry
|
|
5551
|
+
const createWorkspace = props.createWorkspace
|
|
5552
|
+
|
|
5553
|
+
const snapshot = typeof useWorkspaces === 'function' ? useWorkspaces(state => state) : null
|
|
5554
|
+
const groupBy = typeof useStore === 'function'
|
|
5555
|
+
? useStore(state => (state && state.groupBy) || 'workspace-tree')
|
|
5556
|
+
: 'workspace-tree'
|
|
5557
|
+
const groupMode = typeof useStore === 'function'
|
|
5558
|
+
? useStore(state => workspaceGroupModeOf(state && state.workspaceGroupMode, state && state.workspaceTitleSlash))
|
|
5559
|
+
: 'disk'
|
|
5560
|
+
const compactChains = typeof useStore === 'function'
|
|
5561
|
+
? useStore(state => !state || !state.prefs || state.prefs.compactChains !== false)
|
|
5562
|
+
: true
|
|
5563
|
+
const wsFolders = typeof useStore === 'function'
|
|
5564
|
+
? useStore(state => (state && state.folders && state.folders.ws) || EMPTY_FOLDERS)
|
|
5565
|
+
: EMPTY_FOLDERS
|
|
5566
|
+
// Occupancy is the only reactive fact here: the ledger read re-runs on
|
|
5567
|
+
// every render, so the count just forces the re-render when the shipped
|
|
5568
|
+
// occupant loads or unloads (its identity is stable while occupied).
|
|
5569
|
+
const flowCount = useFlowOccupancy(count => count)
|
|
5570
|
+
/** Picker-local collapse: the tree opens fully expanded (a chooser must show its targets) and a folder row folds only here. */
|
|
5571
|
+
const [folded, setFolded] = React.useState(EMPTY_FOLDERS)
|
|
5572
|
+
const [flowOpen, setFlowOpen] = React.useState(false)
|
|
5573
|
+
const [picking, setPicking] = React.useState(false)
|
|
5574
|
+
const [errorText, setErrorText] = React.useState(null)
|
|
5575
|
+
|
|
5576
|
+
const items = snapshot && Array.isArray(snapshot.items) ? snapshot.items : EMPTY_LIST
|
|
5577
|
+
// Same construction the sidebar runs (buildTree + optional chain
|
|
5578
|
+
// compression), so both surfaces answer the same view options.
|
|
5579
|
+
const tree = React.useMemo(() => {
|
|
5580
|
+
const built = buildTree(items, groupMode, wsFolders)
|
|
5581
|
+
if (!compactChains) return built
|
|
5582
|
+
return { ...built, folders: built.folders.map((f) => materializeChain(compressTree(f))), workspaces: built.workspaces }
|
|
5583
|
+
}, [items, groupMode, wsFolders, compactChains])
|
|
5584
|
+
|
|
5585
|
+
const rows = React.useMemo(() => {
|
|
5586
|
+
const out = []
|
|
5587
|
+
const flatMode = groupBy !== 'workspace-tree'
|
|
5588
|
+
const isFolded = (key) => folded[key] === true
|
|
5589
|
+
const toggle = (key) => {
|
|
5590
|
+
setFolded((prev) => {
|
|
5591
|
+
const next = { ...prev }
|
|
5592
|
+
if (next[key] === true) delete next[key]
|
|
5593
|
+
else next[key] = true
|
|
5594
|
+
return next
|
|
5595
|
+
})
|
|
5596
|
+
}
|
|
5597
|
+
const pick = (workspace) => {
|
|
5598
|
+
if (!workspace || workspace.workspaceId === undefined) return
|
|
5599
|
+
if (typeof onPick === 'function') onPick(workspace.workspaceId)
|
|
5600
|
+
}
|
|
5601
|
+
// Tree rows carry the leaf the sidebar shows; flat rows carry the full
|
|
5602
|
+
// title (official 'workspace' mode shows whole titles, and a picker
|
|
5603
|
+
// must stay unambiguous once the folder context is gone).
|
|
5604
|
+
const labelOf = (workspace) => String((flatMode ? workspace.title : workspace.leaf)
|
|
5605
|
+
|| workspace.title || workspace.leaf || '')
|
|
5606
|
+
const workspaceRows = (entry, depth, deep) => {
|
|
5607
|
+
const workspace = entry.workspace || entry
|
|
5608
|
+
out.push(E(PickerRow, {
|
|
5609
|
+
key: 'w:' + String(workspace.workspaceId),
|
|
5610
|
+
depth,
|
|
5611
|
+
glyph: icon('IconFolderCloseRegular', 16),
|
|
5612
|
+
label: labelOf(workspace),
|
|
5613
|
+
checked: workspace.workspaceId === selectedId,
|
|
5614
|
+
onSelect: () => pick(workspace),
|
|
5615
|
+
}))
|
|
5616
|
+
// Disk-nested child workspaces always render: folding them away would
|
|
5617
|
+
// put them out of reach of a chooser.
|
|
5618
|
+
if (deep !== false && entry.sub) levelRows(entry.sub, depth + 1)
|
|
5619
|
+
}
|
|
5620
|
+
const folderRows = (node, depth) => {
|
|
5621
|
+
// compressTree materialises a single-child chain ending in exactly
|
|
5622
|
+
// one workspace as a 'ws' row: that row IS the workspace row.
|
|
5623
|
+
if (node.kind === 'ws') { workspaceRows({ workspace: node.workspace }, depth, true); return }
|
|
5624
|
+
const idPath = node.idPath || node.path
|
|
5625
|
+
const collapsed = isFolded(idPath)
|
|
5626
|
+
out.push(E(PickerRow, {
|
|
5627
|
+
key: 'f:' + String(idPath),
|
|
5628
|
+
depth,
|
|
5629
|
+
glyph: E('span', { className: cls('bw-chevron', !collapsed && 'bw-chevron-open') }, icon('IconTriangleRightFill14', 14)),
|
|
5630
|
+
label: folderLabelOf(node),
|
|
5631
|
+
folder: true,
|
|
5632
|
+
checked: false,
|
|
5633
|
+
onSelect: () => toggle(idPath),
|
|
5634
|
+
}))
|
|
5635
|
+
if (collapsed) return
|
|
5636
|
+
for (const child of node.folders) folderRows(child, depth + 1)
|
|
5637
|
+
for (const entry of node.workspaces) workspaceRows(entry, depth + 1, true)
|
|
5638
|
+
}
|
|
5639
|
+
const levelRows = (level, depth) => {
|
|
5640
|
+
for (const node of level.folders) folderRows(node, depth)
|
|
5641
|
+
for (const entry of level.workspaces) workspaceRows(entry, depth, true)
|
|
5642
|
+
}
|
|
5643
|
+
if (!flatMode) levelRows(tree, 0)
|
|
5644
|
+
else {
|
|
5645
|
+
// Flat surfaces (official 'workspace' mode, and 'flat' where the
|
|
5646
|
+
// sidebar carries no workspace rows at all): one level, full titles —
|
|
5647
|
+
// a picker still has to offer every workspace.
|
|
5648
|
+
const all = []
|
|
5649
|
+
const collect = (level) => {
|
|
5650
|
+
for (const node of level.folders) collect(node)
|
|
5651
|
+
for (const entry of level.workspaces) { all.push(entry); if (entry.sub) collect(entry.sub) }
|
|
5652
|
+
}
|
|
5653
|
+
collect(tree)
|
|
5654
|
+
for (const entry of all) workspaceRows(entry, 0, false)
|
|
5655
|
+
}
|
|
5656
|
+
return out
|
|
5657
|
+
}, [tree, groupBy, folded, selectedId, onPick])
|
|
5658
|
+
|
|
5659
|
+
const flowAvailable = flowCount > 0
|
|
5660
|
+
const addEntry = flowAvailable
|
|
5661
|
+
? { id: HERO_ADD_ID, label: t('add'), icon: icon('IconPlusOutlineRegular', 16) }
|
|
5662
|
+
: null
|
|
5663
|
+
const flowEntry = flowAvailable && typeof heroFlowEntry === 'function' ? heroFlowEntry() : null
|
|
5664
|
+
const flowComponent = flowEntry && flowEntry.component ? flowEntry.component : null
|
|
5665
|
+
let flowFace = null
|
|
5666
|
+
if (flowEntry && typeof flowEntry.inject === 'function') {
|
|
5667
|
+
try { flowFace = flowEntry.inject() } catch (error) { flowFace = null }
|
|
5668
|
+
}
|
|
5669
|
+
const openFlow = () => {
|
|
5670
|
+
setErrorText(null)
|
|
5671
|
+
setFlowOpen(true)
|
|
5672
|
+
if (typeof onClose === 'function') onClose()
|
|
5673
|
+
}
|
|
5674
|
+
const adoptPath = (path) => {
|
|
5675
|
+
setPicking(true)
|
|
5676
|
+
Promise.resolve()
|
|
5677
|
+
.then(() => createWorkspace({ path: String(path) }))
|
|
5678
|
+
.then((workspace) => {
|
|
5679
|
+
setFlowOpen(false)
|
|
5680
|
+
if (workspace && workspace.workspaceId !== undefined && typeof onPick === 'function') onPick(workspace.workspaceId)
|
|
5681
|
+
})
|
|
5682
|
+
.catch((reason) => {
|
|
5683
|
+
// The picker's own error surface: the shared picker flow answers a
|
|
5684
|
+
// failed adoption with a retryable notice, and so does this one.
|
|
5685
|
+
setFlowOpen(false)
|
|
5686
|
+
setErrorText(messageOf(reason))
|
|
5687
|
+
})
|
|
5688
|
+
.then(() => { setPicking(false) })
|
|
5689
|
+
}
|
|
5690
|
+
const menuReady = typeof ui.Menu === 'function'
|
|
5691
|
+
const menuOpen = props.open === true && (rows.length > 0 || addEntry !== null)
|
|
5692
|
+
const flowNode = flowComponent && flowOpen
|
|
5693
|
+
? E(flowComponent, {
|
|
5694
|
+
key: 'bw-hero-flow',
|
|
5695
|
+
open: true,
|
|
5696
|
+
busy: picking,
|
|
5697
|
+
onPicked: adoptPath,
|
|
5698
|
+
onCancel: () => { setFlowOpen(false) },
|
|
5699
|
+
onError: (message) => { setFlowOpen(false); setErrorText(String(message)) },
|
|
5700
|
+
...(flowFace || EMPTY_FOLDERS),
|
|
5701
|
+
})
|
|
5702
|
+
: null
|
|
5703
|
+
const errorNode = menuReady && errorText !== null
|
|
5704
|
+
? E(ui.Modal, {
|
|
5705
|
+
open: true,
|
|
5706
|
+
title: t('heroPicker.error.title'),
|
|
5707
|
+
closeLabel: t('close'),
|
|
5708
|
+
onClose: () => { setErrorText(null) },
|
|
5709
|
+
footer: E('div', { className: 'bw-modal-actions' },
|
|
5710
|
+
E(BTN, { variant: 'outline', onClick: () => { setErrorText(null) } }, t('cancel')),
|
|
5711
|
+
E(BTN, { variant: 'primary', disabled: !flowAvailable, onClick: openFlow }, t('heroPicker.retry')),
|
|
5712
|
+
),
|
|
5713
|
+
}, E('div', { className: 'bw-modal-body' }, E('div', { role: 'alert' }, errorText)))
|
|
5714
|
+
: null
|
|
5715
|
+
return E(React.Fragment, null,
|
|
5716
|
+
menuReady ? E(ui.Menu, {
|
|
5717
|
+
open: menuOpen,
|
|
5718
|
+
// portal mode anchored on the owner's chip: the menu is the popover,
|
|
5719
|
+
// never the trigger (ui-conversation renders the chip itself).
|
|
5720
|
+
anchor: null,
|
|
5721
|
+
items: EMPTY_LIST,
|
|
5722
|
+
...(addEntry ? { footer: [addEntry] } : {}),
|
|
5723
|
+
portal: true,
|
|
5724
|
+
dense: true,
|
|
5725
|
+
side: 'bottom',
|
|
5726
|
+
getAnchorRect: () => (anchorRef && anchorRef.current ? anchorRef.current.getBoundingClientRect() : null),
|
|
5727
|
+
onClose: () => { if (typeof onClose === 'function') onClose() },
|
|
5728
|
+
onSelect: (id) => { if (id === HERO_ADD_ID) openFlow() },
|
|
5729
|
+
}, rows) : null,
|
|
5730
|
+
flowNode,
|
|
5731
|
+
errorNode,
|
|
5732
|
+
)
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5420
5735
|
/* ============================= browser ============================ */
|
|
5421
5736
|
|
|
5422
5737
|
function BetterBrowser(props) {
|
|
@@ -7295,6 +7610,9 @@ window.__ModuleLoader__.load({
|
|
|
7295
7610
|
E(CustomizeDialog, {
|
|
7296
7611
|
open: customize !== null,
|
|
7297
7612
|
kind: customize ? customize.kind : undefined,
|
|
7613
|
+
// Row identity, not the rebuilt initial object, is what re-seeds the
|
|
7614
|
+
// draft (see the dialog's seed effect).
|
|
7615
|
+
seedKey: customize ? customize.entryKey : undefined,
|
|
7298
7616
|
defaults: defaultAppearance,
|
|
7299
7617
|
initial: customize
|
|
7300
7618
|
? { ...appearanceOf(customize.entryKey), icon: (styleEntry(customize.entryKey) || {}).icon || 'solid' }
|
|
@@ -7575,6 +7893,47 @@ window.__ModuleLoader__.load({
|
|
|
7575
7893
|
},
|
|
7576
7894
|
BetterBrowser,
|
|
7577
7895
|
))
|
|
7896
|
+
|
|
7897
|
+
// The HERO hole (v0.22.0): the shipped picker is shadowed at priority -1
|
|
7898
|
+
// and this seat draws the sidebar's own tree instead of a flat title
|
|
7899
|
+
// list. Same rules as the sidebar seat — NO children declaration (the
|
|
7900
|
+
// shipped picker entry keeps owning HERO_FLOW_HOLE even while shadowed,
|
|
7901
|
+
// and re-declaring throws), so the add action renders the shipped
|
|
7902
|
+
// occupant component read off the ledger (see HeroWorkspacePicker).
|
|
7903
|
+
// Hosts without a primitives Menu keep the shipped picker: shadowing
|
|
7904
|
+
// there would leave the hero with no popover at all.
|
|
7905
|
+
if (typeof ui.Menu === 'function') {
|
|
7906
|
+
const heroInjected = () => ({
|
|
7907
|
+
createWorkspace: (input) => workspaces.create(input),
|
|
7908
|
+
/**
|
|
7909
|
+
* The shipped occupant's ledger entry for the hero flow hole: the
|
|
7910
|
+
* renderer picks the lowest-priority live entry, and so does this
|
|
7911
|
+
* read (entries arrive priority-sorted). Best-effort by design — a
|
|
7912
|
+
* host that hides the ledger costs the add action, never the picker.
|
|
7913
|
+
*/
|
|
7914
|
+
heroFlowEntry: () => {
|
|
7915
|
+
try {
|
|
7916
|
+
const list = slots.entries(HERO_FLOW_HOLE)
|
|
7917
|
+
const entry = Array.isArray(list) && list.length > 0 ? list[0] : null
|
|
7918
|
+
return entry && entry.component ? entry : null
|
|
7919
|
+
} catch (error) {
|
|
7920
|
+
return null
|
|
7921
|
+
}
|
|
7922
|
+
},
|
|
7923
|
+
hooks: { directoryFlow: flowSource(slots, HERO_FLOW_HOLE) },
|
|
7924
|
+
})
|
|
7925
|
+
slots.inject('conversation.hero.workspace', guarded(
|
|
7926
|
+
'conversation.hero.workspace',
|
|
7927
|
+
{
|
|
7928
|
+
name: 'conversation.hero.workspace',
|
|
7929
|
+
priority: -1,
|
|
7930
|
+
store: viewStore,
|
|
7931
|
+
inject: heroInjected,
|
|
7932
|
+
locale: NS,
|
|
7933
|
+
},
|
|
7934
|
+
HeroWorkspacePicker,
|
|
7935
|
+
))
|
|
7936
|
+
}
|
|
7578
7937
|
}
|
|
7579
7938
|
|
|
7580
7939
|
return {
|