dsh-multiview 0.0.0-stage → 0.1.8
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/CHANGELOG.md +105 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/README.zh.md +91 -0
- package/client.js +2250 -0
- package/cordis.patch.yml +7 -0
- package/docs/DESIGN.md +81 -0
- package/docs/RELEASE_ACCEPTANCE.md +106 -0
- package/icon.svg +8 -0
- package/index.js +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
package/client.js
ADDED
|
@@ -0,0 +1,2250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-multiview 鈥?Client half.
|
|
3
|
+
* Generated from src/client/*.ts by scripts/build.mjs. Do not edit by hand.
|
|
4
|
+
*/
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-multiview',
|
|
7
|
+
factory(require) {
|
|
8
|
+
const React = require('react')
|
|
9
|
+
const h = React.createElement
|
|
10
|
+
const NS = 'multiview';
|
|
11
|
+
const MV_PANEL_ID = 'multiview';
|
|
12
|
+
const MV_PANE_SLOT = 'multiview.pane';
|
|
13
|
+
const MV_STORAGE_KEY = 'dsh.multiview.v1';
|
|
14
|
+
const MV_PLUGIN_VERSION = '0.1.8';
|
|
15
|
+
const MV_ZH = {
|
|
16
|
+
panel: '分屏',
|
|
17
|
+
title: '分屏',
|
|
18
|
+
empty: '选择一个会话',
|
|
19
|
+
pick: '选择会话',
|
|
20
|
+
pickSearch: '搜索会话',
|
|
21
|
+
pickEmpty: '没有匹配的会话',
|
|
22
|
+
loading: '正在打开会话…',
|
|
23
|
+
openFailed: '打开会话失败:{message}',
|
|
24
|
+
noContent: '当前 DSH 版本不支持在格内嵌入会话',
|
|
25
|
+
paneCrashed: '这一格出错了:{message}',
|
|
26
|
+
stageCrashed: '分屏面板出错了:{message}',
|
|
27
|
+
maximize: '最大化',
|
|
28
|
+
restore: '还原',
|
|
29
|
+
remove: '清空这一格',
|
|
30
|
+
openMain: '在原生视图打开',
|
|
31
|
+
shape: '格数',
|
|
32
|
+
shapeN: '{n} 格',
|
|
33
|
+
running: '运行中',
|
|
34
|
+
waiting: '等待你处理',
|
|
35
|
+
unread: '有新结果',
|
|
36
|
+
views: '视图',
|
|
37
|
+
viewsEmpty: '还没有保存的视图。绑定两个以上会话后会自动记住。',
|
|
38
|
+
viewsCurrent: '当前分屏',
|
|
39
|
+
viewsDelete: '删除视图',
|
|
40
|
+
viewsHint: '点击恢复这个分屏',
|
|
41
|
+
updateCheck: '检查更新',
|
|
42
|
+
updateChecking: '正在检查更新…',
|
|
43
|
+
updateLatest: '已是最新版本({version})',
|
|
44
|
+
updateAvailable: '发现新版本 {version}',
|
|
45
|
+
updateInstall: '立即更新',
|
|
46
|
+
updateInstalling: '正在更新…',
|
|
47
|
+
updateDone: '更新完成,重启 DSH 后生效',
|
|
48
|
+
updateRestart: '重启 DSH',
|
|
49
|
+
updateFailed: '检查更新失败:{message}',
|
|
50
|
+
updateUnsupported: '未检测到插件市场,无法自动更新',
|
|
51
|
+
updateLocal: '当前为本地链接安装,自动更新不可用',
|
|
52
|
+
updateIdle: '尚未检查更新',
|
|
53
|
+
cancel: '取消',
|
|
54
|
+
close: '关闭',
|
|
55
|
+
shortcutFocusPrev: '聚焦上一格',
|
|
56
|
+
shortcutFocusNext: '聚焦下一格',
|
|
57
|
+
shortcutMaximize: '最大化或还原当前格',
|
|
58
|
+
shortcutRemove: '从分屏移除当前格'
|
|
59
|
+
};
|
|
60
|
+
const MV_EN = {
|
|
61
|
+
panel: 'Split',
|
|
62
|
+
title: 'Split',
|
|
63
|
+
empty: 'Pick a session',
|
|
64
|
+
pick: 'Pick a session',
|
|
65
|
+
pickSearch: 'Search sessions',
|
|
66
|
+
pickEmpty: 'No matching session',
|
|
67
|
+
loading: 'Opening session…',
|
|
68
|
+
openFailed: 'Could not open this session: {message}',
|
|
69
|
+
noContent: 'This DSH version cannot host a conversation in a pane',
|
|
70
|
+
paneCrashed: 'This pane failed: {message}',
|
|
71
|
+
stageCrashed: 'The split view failed: {message}',
|
|
72
|
+
maximize: 'Maximize',
|
|
73
|
+
restore: 'Restore',
|
|
74
|
+
remove: 'Clear this pane',
|
|
75
|
+
openMain: 'Open in native view',
|
|
76
|
+
shape: 'Pane count',
|
|
77
|
+
shapeN: '{n} panes',
|
|
78
|
+
running: 'Running',
|
|
79
|
+
waiting: 'Waiting for you',
|
|
80
|
+
unread: 'New result',
|
|
81
|
+
views: 'Views',
|
|
82
|
+
viewsEmpty: 'No saved views yet. A split with two or more sessions is remembered automatically.',
|
|
83
|
+
viewsCurrent: 'Current split',
|
|
84
|
+
viewsDelete: 'Delete view',
|
|
85
|
+
viewsHint: 'Restore this split',
|
|
86
|
+
updateCheck: 'Check for updates',
|
|
87
|
+
updateChecking: 'Checking for updates…',
|
|
88
|
+
updateLatest: 'Up to date ({version})',
|
|
89
|
+
updateAvailable: 'Version {version} is available',
|
|
90
|
+
updateInstall: 'Update now',
|
|
91
|
+
updateInstalling: 'Updating…',
|
|
92
|
+
updateDone: 'Updated — restart DSH to apply',
|
|
93
|
+
updateRestart: 'Restart DSH',
|
|
94
|
+
updateFailed: 'Update check failed: {message}',
|
|
95
|
+
updateUnsupported: 'No plugin market detected; automatic updates unavailable',
|
|
96
|
+
updateLocal: 'Installed from a local link; automatic updates unavailable',
|
|
97
|
+
updateIdle: 'Not checked yet',
|
|
98
|
+
cancel: 'Cancel',
|
|
99
|
+
close: 'Close',
|
|
100
|
+
shortcutFocusPrev: 'Focus previous pane',
|
|
101
|
+
shortcutFocusNext: 'Focus next pane',
|
|
102
|
+
shortcutMaximize: 'Maximize or restore the current pane',
|
|
103
|
+
shortcutRemove: 'Remove the current pane from the split'
|
|
104
|
+
};
|
|
105
|
+
let PTX = null;
|
|
106
|
+
let T = function translateFallback(key) {
|
|
107
|
+
return MV_ZH[key] === undefined ? key : MV_ZH[key];
|
|
108
|
+
};
|
|
109
|
+
function mvFormat(template, params) {
|
|
110
|
+
if (params === undefined) return template;
|
|
111
|
+
return template.replace(/\{(\w+)\}/g, (match, name)=>params[name] === undefined ? match : String(params[name]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mvConversationService() {
|
|
115
|
+
try {
|
|
116
|
+
const direct = PTX.conversation;
|
|
117
|
+
if (direct !== undefined && direct !== null) return direct;
|
|
118
|
+
} catch (error) {}
|
|
119
|
+
try {
|
|
120
|
+
const get = PTX.get;
|
|
121
|
+
return typeof get === 'function' ? get.call(PTX, 'conversation') : undefined;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function mvFocusComposer(sessionId) {
|
|
127
|
+
if (sessionId === null || sessionId === undefined) return false;
|
|
128
|
+
try {
|
|
129
|
+
const scoped = PTX.sessions.scope(sessionId);
|
|
130
|
+
if (scoped === undefined || scoped === null) return false;
|
|
131
|
+
const conversation = mvConversationService();
|
|
132
|
+
const resolver = conversation === undefined ? undefined : conversation.input;
|
|
133
|
+
if (resolver === undefined || resolver === null || typeof resolver.for !== 'function') return false;
|
|
134
|
+
const facade = resolver.for(scoped);
|
|
135
|
+
if (facade === undefined || facade === null || typeof facade.focus !== 'function') return false;
|
|
136
|
+
facade.focus();
|
|
137
|
+
return true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function mvConversationLive() {
|
|
143
|
+
try {
|
|
144
|
+
const slots = PTX.slots;
|
|
145
|
+
if (slots === undefined || slots === null) return false;
|
|
146
|
+
if (typeof slots.spec === 'function' && slots.spec('conversation.session') !== undefined) return true;
|
|
147
|
+
if (typeof slots.specDynamic === 'function' && slots.specDynamic('conversation.session') !== undefined) return true;
|
|
148
|
+
return typeof slots.entries === 'function' && slots.entries('conversation.session').length > 0;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function mvRow(sessionId) {
|
|
154
|
+
if (sessionId === null || sessionId === undefined) return undefined;
|
|
155
|
+
try {
|
|
156
|
+
return PTX.sessions.list.getSnapshot().byId[sessionId];
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function mvTitle(sessionId) {
|
|
162
|
+
const row = mvRow(sessionId);
|
|
163
|
+
return row === undefined ? undefined : row.title;
|
|
164
|
+
}
|
|
165
|
+
function mvIsArchived(sessionId) {
|
|
166
|
+
try {
|
|
167
|
+
const archived = PTX.workspaces.list.getSnapshot().archivedSessionIds;
|
|
168
|
+
return archived.includes(sessionId);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function mvSeed(limit) {
|
|
174
|
+
const picked = [];
|
|
175
|
+
try {
|
|
176
|
+
const list = PTX.sessions.list.getSnapshot();
|
|
177
|
+
const current = list.ids.find((id)=>(list.byId[id]?.retainedBy?.mainView ?? 0) > 0);
|
|
178
|
+
const ordered = current === undefined ? list.ids.slice() : [
|
|
179
|
+
current,
|
|
180
|
+
...list.ids
|
|
181
|
+
];
|
|
182
|
+
for (const id of ordered){
|
|
183
|
+
if (picked.length >= limit) break;
|
|
184
|
+
if (picked.includes(id)) continue;
|
|
185
|
+
const row = list.byId[id];
|
|
186
|
+
if (row === undefined || row.blank === true) continue;
|
|
187
|
+
if (mvIsArchived(id)) continue;
|
|
188
|
+
picked.push(id);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {}
|
|
191
|
+
return picked;
|
|
192
|
+
}
|
|
193
|
+
function mvTopUp(current, limit) {
|
|
194
|
+
const next = current.slice(0, limit);
|
|
195
|
+
while(next.length < limit)next.push(null);
|
|
196
|
+
let changed = next.length !== current.length;
|
|
197
|
+
const used = new Set(next.filter((id)=>id !== null && id !== undefined));
|
|
198
|
+
try {
|
|
199
|
+
const list = PTX.sessions.list.getSnapshot();
|
|
200
|
+
const current = list.ids.find((id)=>(list.byId[id]?.retainedBy?.mainView ?? 0) > 0);
|
|
201
|
+
const ordered = current === undefined ? list.ids.slice() : [
|
|
202
|
+
current,
|
|
203
|
+
...list.ids
|
|
204
|
+
];
|
|
205
|
+
for(let index = 0; index < next.length; index += 1){
|
|
206
|
+
if (next[index] !== null && next[index] !== undefined) continue;
|
|
207
|
+
const candidate = ordered.find((id)=>{
|
|
208
|
+
if (used.has(id)) return false;
|
|
209
|
+
const row = list.byId[id];
|
|
210
|
+
return row !== undefined && row.blank !== true && !mvIsArchived(id);
|
|
211
|
+
});
|
|
212
|
+
if (candidate === undefined) continue;
|
|
213
|
+
next[index] = candidate;
|
|
214
|
+
used.add(candidate);
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {}
|
|
218
|
+
return changed ? next : current;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const MV_MAX_PANES = 4;
|
|
222
|
+
const MV_MIN_RATIO = 0.22;
|
|
223
|
+
const MV_MAX_RATIO = 0.78;
|
|
224
|
+
function mvClampRatio(value) {
|
|
225
|
+
if (!Number.isFinite(value)) return 0.5;
|
|
226
|
+
return Math.min(MV_MAX_RATIO, Math.max(MV_MIN_RATIO, value));
|
|
227
|
+
}
|
|
228
|
+
function mvLeaf(id) {
|
|
229
|
+
return {
|
|
230
|
+
kind: 'leaf',
|
|
231
|
+
id,
|
|
232
|
+
sessionId: null
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function mvSplit(id, dir, ratio, a, b) {
|
|
236
|
+
return {
|
|
237
|
+
kind: 'split',
|
|
238
|
+
id,
|
|
239
|
+
dir,
|
|
240
|
+
ratio: mvClampRatio(ratio),
|
|
241
|
+
a,
|
|
242
|
+
b
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function mvPreset(count) {
|
|
246
|
+
if (count <= 2) return mvSplit('s1', 'row', 0.5, mvLeaf('p1'), mvLeaf('p2'));
|
|
247
|
+
if (count === 3) {
|
|
248
|
+
return mvSplit('s1', 'row', 0.55, mvLeaf('p1'), mvSplit('s2', 'col', 0.5, mvLeaf('p2'), mvLeaf('p3')));
|
|
249
|
+
}
|
|
250
|
+
return mvSplit('s1', 'row', 0.5, mvSplit('s2', 'col', 0.5, mvLeaf('p1'), mvLeaf('p2')), mvSplit('s3', 'col', 0.5, mvLeaf('p3'), mvLeaf('p4')));
|
|
251
|
+
}
|
|
252
|
+
function mvLeaves(node, out) {
|
|
253
|
+
const list = out === undefined ? [] : out;
|
|
254
|
+
if (node === undefined || node === null) return list;
|
|
255
|
+
if (node.kind === 'leaf') {
|
|
256
|
+
list.push(node);
|
|
257
|
+
return list;
|
|
258
|
+
}
|
|
259
|
+
mvLeaves(node.a, list);
|
|
260
|
+
mvLeaves(node.b, list);
|
|
261
|
+
return list;
|
|
262
|
+
}
|
|
263
|
+
function mvLeafCount(node) {
|
|
264
|
+
return mvLeaves(node).length;
|
|
265
|
+
}
|
|
266
|
+
function mvShapeTo(node, count) {
|
|
267
|
+
const target = Math.max(2, Math.min(MV_MAX_PANES, count));
|
|
268
|
+
const current = mvLeaves(node);
|
|
269
|
+
if (current.length === target) return node;
|
|
270
|
+
const sessions = current.map((leaf)=>leaf.sessionId);
|
|
271
|
+
const next = mvPreset(target);
|
|
272
|
+
const leaves = mvLeaves(next);
|
|
273
|
+
for(let index = 0; index < leaves.length; index += 1)leaves[index].sessionId = sessions[index] ?? null;
|
|
274
|
+
return next;
|
|
275
|
+
}
|
|
276
|
+
function mvMapLeaves(node, edit) {
|
|
277
|
+
if (node.kind === 'leaf') return edit(node);
|
|
278
|
+
const a = mvMapLeaves(node.a, edit);
|
|
279
|
+
const b = mvMapLeaves(node.b, edit);
|
|
280
|
+
if (a === node.a && b === node.b) return node;
|
|
281
|
+
return {
|
|
282
|
+
...node,
|
|
283
|
+
a,
|
|
284
|
+
b
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function mvWithSession(node, leafId, sessionId) {
|
|
288
|
+
return mvMapLeaves(node, (leaf)=>{
|
|
289
|
+
if (leaf.id !== leafId && sessionId !== null && leaf.sessionId === sessionId) return {
|
|
290
|
+
...leaf,
|
|
291
|
+
sessionId: null
|
|
292
|
+
};
|
|
293
|
+
if (leaf.id !== leafId) return leaf;
|
|
294
|
+
if (leaf.sessionId === sessionId) return leaf;
|
|
295
|
+
return {
|
|
296
|
+
...leaf,
|
|
297
|
+
sessionId
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function mvSwapSessions(node, aId, bId) {
|
|
302
|
+
if (aId === bId) return node;
|
|
303
|
+
const leaves = mvLeaves(node);
|
|
304
|
+
const a = leaves.find((leaf)=>leaf.id === aId);
|
|
305
|
+
const b = leaves.find((leaf)=>leaf.id === bId);
|
|
306
|
+
if (a === undefined || b === undefined) return node;
|
|
307
|
+
const aSession = a.sessionId;
|
|
308
|
+
const bSession = b.sessionId;
|
|
309
|
+
if (aSession === bSession) return node;
|
|
310
|
+
return mvMapLeaves(node, (leaf)=>{
|
|
311
|
+
if (leaf.id === aId) return {
|
|
312
|
+
...leaf,
|
|
313
|
+
sessionId: bSession
|
|
314
|
+
};
|
|
315
|
+
if (leaf.id === bId) return {
|
|
316
|
+
...leaf,
|
|
317
|
+
sessionId: aSession
|
|
318
|
+
};
|
|
319
|
+
return leaf;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function mvResize(node, splitId, ratio) {
|
|
323
|
+
if (node === null || node === undefined) return node;
|
|
324
|
+
if (node.kind === 'leaf') return node;
|
|
325
|
+
if (node.id === splitId) {
|
|
326
|
+
const clamped = mvClampRatio(ratio);
|
|
327
|
+
return clamped === node.ratio ? node : {
|
|
328
|
+
...node,
|
|
329
|
+
ratio: clamped
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const a = mvResize(node.a, splitId, ratio);
|
|
333
|
+
const b = mvResize(node.b, splitId, ratio);
|
|
334
|
+
if (a === node.a && b === node.b) return node;
|
|
335
|
+
return {
|
|
336
|
+
...node,
|
|
337
|
+
a,
|
|
338
|
+
b
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function mvRemoveLeaf(node, leafId) {
|
|
342
|
+
if (node === null || node === undefined) return null;
|
|
343
|
+
if (node.kind === 'leaf') return node.id === leafId ? null : node;
|
|
344
|
+
const a = mvRemoveLeaf(node.a, leafId);
|
|
345
|
+
const b = mvRemoveLeaf(node.b, leafId);
|
|
346
|
+
if (a === null) return b;
|
|
347
|
+
if (b === null) return a;
|
|
348
|
+
if (a === node.a && b === node.b) return node;
|
|
349
|
+
return {
|
|
350
|
+
...node,
|
|
351
|
+
a,
|
|
352
|
+
b
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function mvLeafById(node, leafId) {
|
|
356
|
+
return mvLeaves(node).find((leaf)=>leaf.id === leafId);
|
|
357
|
+
}
|
|
358
|
+
function mvNeighbour(node, leafId, delta) {
|
|
359
|
+
const leaves = mvLeaves(node);
|
|
360
|
+
const index = leaves.findIndex((leaf)=>leaf.id === leafId);
|
|
361
|
+
if (index < 0 || leaves.length < 2) return leafId;
|
|
362
|
+
const next = (index + delta + leaves.length) % leaves.length;
|
|
363
|
+
return leaves[next].id;
|
|
364
|
+
}
|
|
365
|
+
function mvIsLeaf(value) {
|
|
366
|
+
return value !== null && typeof value === 'object' && value.kind === 'leaf' && typeof value.id === 'string' && (value.sessionId === null || typeof value.sessionId === 'string');
|
|
367
|
+
}
|
|
368
|
+
function mvIsSplit(value) {
|
|
369
|
+
if (value === null || typeof value !== 'object' || value.kind !== 'split') return false;
|
|
370
|
+
if (typeof value.id !== 'string') return false;
|
|
371
|
+
if (value.dir !== 'row' && value.dir !== 'col') return false;
|
|
372
|
+
if (typeof value.ratio !== 'number' || !Number.isFinite(value.ratio)) return false;
|
|
373
|
+
return value.a !== null && value.b !== null && typeof value.a === 'object' && typeof value.b === 'object';
|
|
374
|
+
}
|
|
375
|
+
function mvNormalizeIds(node, seen) {
|
|
376
|
+
if (node === null || node === undefined) return node;
|
|
377
|
+
if (node.kind === 'leaf') {
|
|
378
|
+
if (seen.has(node.id)) return {
|
|
379
|
+
...node,
|
|
380
|
+
id: 'p' + String(seen.size + 1)
|
|
381
|
+
};
|
|
382
|
+
seen.add(node.id);
|
|
383
|
+
return node;
|
|
384
|
+
}
|
|
385
|
+
const a = mvNormalizeIds(node.a, seen);
|
|
386
|
+
const b = mvNormalizeIds(node.b, seen);
|
|
387
|
+
const id = seen.has(node.id) ? 's' + String(seen.size + 1) : node.id;
|
|
388
|
+
seen.add(id);
|
|
389
|
+
if (a === node.a && b === node.b && id === node.id) return node;
|
|
390
|
+
return {
|
|
391
|
+
...node,
|
|
392
|
+
id,
|
|
393
|
+
a,
|
|
394
|
+
b
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function mvSanitize(raw) {
|
|
398
|
+
const walk = (value, depth)=>{
|
|
399
|
+
if (depth > 8) return null;
|
|
400
|
+
if (mvIsLeaf(value)) return {
|
|
401
|
+
kind: 'leaf',
|
|
402
|
+
id: value.id,
|
|
403
|
+
sessionId: value.sessionId
|
|
404
|
+
};
|
|
405
|
+
if (!mvIsSplit(value)) return null;
|
|
406
|
+
const a = walk(value.a, depth + 1);
|
|
407
|
+
const b = walk(value.b, depth + 1);
|
|
408
|
+
if (a === null || b === null) return null;
|
|
409
|
+
const leaves = mvLeaves(a).length + mvLeaves(b).length;
|
|
410
|
+
if (leaves > MV_MAX_PANES) return a;
|
|
411
|
+
return {
|
|
412
|
+
kind: 'split',
|
|
413
|
+
id: value.id,
|
|
414
|
+
dir: value.dir,
|
|
415
|
+
ratio: mvClampRatio(value.ratio),
|
|
416
|
+
a,
|
|
417
|
+
b
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
const tree = walk(raw, 0);
|
|
421
|
+
if (tree === null) return null;
|
|
422
|
+
if (tree.kind === 'leaf') return tree;
|
|
423
|
+
return mvNormalizeIds(tree, new Set());
|
|
424
|
+
}
|
|
425
|
+
function mvReconcileTree(node, reconcile) {
|
|
426
|
+
return mvMapLeaves(node, (leaf)=>{
|
|
427
|
+
if (leaf.sessionId === null) return leaf;
|
|
428
|
+
const kept = reconcile(leaf.sessionId);
|
|
429
|
+
if (kept === leaf.sessionId) return leaf;
|
|
430
|
+
return {
|
|
431
|
+
...leaf,
|
|
432
|
+
sessionId: kept
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function mvInitialState() {
|
|
438
|
+
return {
|
|
439
|
+
layout: mvPreset(2),
|
|
440
|
+
focused: 'p1',
|
|
441
|
+
maximized: null,
|
|
442
|
+
preset: 2,
|
|
443
|
+
seeded: false,
|
|
444
|
+
reconciled: false,
|
|
445
|
+
views: [],
|
|
446
|
+
currentViewId: null
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
let mvState = mvInitialState();
|
|
450
|
+
const mvListeners = new Set();
|
|
451
|
+
let mvSaveTimer = 0;
|
|
452
|
+
function mvGetState() {
|
|
453
|
+
return mvState;
|
|
454
|
+
}
|
|
455
|
+
function mvSubscribe(listener) {
|
|
456
|
+
mvListeners.add(listener);
|
|
457
|
+
return ()=>{
|
|
458
|
+
mvListeners.delete(listener);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function mvScheduleSave() {
|
|
462
|
+
if (mvSaveTimer !== 0) clearTimeout(mvSaveTimer);
|
|
463
|
+
mvSaveTimer = setTimeout(()=>{
|
|
464
|
+
mvSaveTimer = 0;
|
|
465
|
+
try {
|
|
466
|
+
const durable = {
|
|
467
|
+
version: 3,
|
|
468
|
+
layout: mvState.layout,
|
|
469
|
+
focused: mvState.focused,
|
|
470
|
+
preset: mvState.preset,
|
|
471
|
+
seeded: mvState.seeded,
|
|
472
|
+
views: mvState.views,
|
|
473
|
+
currentViewId: mvState.currentViewId
|
|
474
|
+
};
|
|
475
|
+
window.localStorage.setItem(MV_STORAGE_KEY, JSON.stringify(durable));
|
|
476
|
+
} catch (error) {}
|
|
477
|
+
}, 200);
|
|
478
|
+
}
|
|
479
|
+
function mvViewKey(layout) {
|
|
480
|
+
return mvLeaves(layout).map((leaf)=>leaf.sessionId).filter((sessionId)=>sessionId !== null).join(',');
|
|
481
|
+
}
|
|
482
|
+
function mvViewName(layout) {
|
|
483
|
+
let list = null;
|
|
484
|
+
try {
|
|
485
|
+
list = PTX.sessions.list.getSnapshot();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
list = null;
|
|
488
|
+
}
|
|
489
|
+
const titles = mvLeaves(layout).map((leaf)=>leaf.sessionId === null ? null : list?.byId[leaf.sessionId]?.title).filter((title)=>typeof title === 'string' && title.length > 0);
|
|
490
|
+
if (titles.length === 0) return T('viewsCurrent');
|
|
491
|
+
return titles.join(' | ');
|
|
492
|
+
}
|
|
493
|
+
function mvViewWorthy(layout) {
|
|
494
|
+
const key = mvViewKey(layout);
|
|
495
|
+
return key !== '' && key.indexOf(',') >= 0;
|
|
496
|
+
}
|
|
497
|
+
function mvSyncView(next) {
|
|
498
|
+
const views = next.views ?? [];
|
|
499
|
+
const worthy = mvViewWorthy(next.layout);
|
|
500
|
+
const current = views.find((view)=>view.id === next.currentViewId) ?? null;
|
|
501
|
+
if (!worthy) {
|
|
502
|
+
if (current === null) return next;
|
|
503
|
+
return {
|
|
504
|
+
...next,
|
|
505
|
+
views: views.filter((view)=>view.id !== current.id),
|
|
506
|
+
currentViewId: null
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (current !== null) {
|
|
510
|
+
return {
|
|
511
|
+
...next,
|
|
512
|
+
views: views.map((view)=>view.id === current.id ? {
|
|
513
|
+
...view,
|
|
514
|
+
layout: next.layout,
|
|
515
|
+
name: mvViewName(next.layout)
|
|
516
|
+
} : view)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const key = mvViewKey(next.layout);
|
|
520
|
+
const existing = views.find((view)=>view.id === key);
|
|
521
|
+
if (existing !== undefined) {
|
|
522
|
+
return {
|
|
523
|
+
...next,
|
|
524
|
+
currentViewId: existing.id,
|
|
525
|
+
views: views.map((view)=>view.id === existing.id ? {
|
|
526
|
+
...view,
|
|
527
|
+
layout: next.layout,
|
|
528
|
+
name: mvViewName(next.layout)
|
|
529
|
+
} : view)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const created = {
|
|
533
|
+
id: key,
|
|
534
|
+
name: mvViewName(next.layout),
|
|
535
|
+
layout: next.layout,
|
|
536
|
+
savedAt: Date.now()
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
...next,
|
|
540
|
+
currentViewId: created.id,
|
|
541
|
+
views: [
|
|
542
|
+
created,
|
|
543
|
+
...views
|
|
544
|
+
].slice(0, 12)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function mvCommit(produce) {
|
|
548
|
+
const produced = produce(mvState);
|
|
549
|
+
if (produced === mvState || produced === null || produced === undefined) return;
|
|
550
|
+
mvState = mvSyncView(produced);
|
|
551
|
+
mvScheduleSave();
|
|
552
|
+
for (const listener of mvListeners){
|
|
553
|
+
try {
|
|
554
|
+
listener();
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error('dsh-multiview: state listener failed', error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function mvSanitizeView(raw) {
|
|
561
|
+
if (raw === null || typeof raw !== 'object') return null;
|
|
562
|
+
const layout = mvSanitize(raw.layout);
|
|
563
|
+
if (layout === null || !mvViewWorthy(layout)) return null;
|
|
564
|
+
return {
|
|
565
|
+
id: typeof raw.id === 'string' && raw.id.length > 0 ? raw.id : mvViewKey(layout),
|
|
566
|
+
name: typeof raw.name === 'string' && raw.name.length > 0 ? raw.name : T('viewsCurrent'),
|
|
567
|
+
layout,
|
|
568
|
+
savedAt: typeof raw.savedAt === 'number' ? raw.savedAt : Date.now()
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function mvHydrate() {
|
|
572
|
+
let raw = null;
|
|
573
|
+
try {
|
|
574
|
+
raw = JSON.parse(window.localStorage.getItem(MV_STORAGE_KEY) ?? 'null');
|
|
575
|
+
} catch (error) {
|
|
576
|
+
raw = null;
|
|
577
|
+
}
|
|
578
|
+
if (raw === null || typeof raw !== 'object') return;
|
|
579
|
+
if (raw.version !== 1 && raw.version !== 2 && raw.version !== 3) return;
|
|
580
|
+
const layout = mvSanitize(raw.layout);
|
|
581
|
+
if (layout === null) return;
|
|
582
|
+
const leaves = mvLeaves(layout);
|
|
583
|
+
const focused = leaves.some((leaf)=>leaf.id === raw.focused) ? raw.focused : leaves[0].id;
|
|
584
|
+
const views = Array.isArray(raw.views) ? raw.views.map(mvSanitizeView).filter((view)=>view !== null) : [];
|
|
585
|
+
const currentViewId = typeof raw.currentViewId === 'string' && views.some((view)=>view.id === raw.currentViewId) ? raw.currentViewId : null;
|
|
586
|
+
mvState = {
|
|
587
|
+
layout,
|
|
588
|
+
focused,
|
|
589
|
+
maximized: null,
|
|
590
|
+
preset: typeof raw.preset === 'number' ? raw.preset : leaves.length,
|
|
591
|
+
seeded: raw.seeded === true,
|
|
592
|
+
reconciled: false,
|
|
593
|
+
views,
|
|
594
|
+
currentViewId
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function mvReset() {
|
|
598
|
+
const views = mvState.views;
|
|
599
|
+
mvCommit(()=>({
|
|
600
|
+
...mvInitialState(),
|
|
601
|
+
views,
|
|
602
|
+
currentViewId: null
|
|
603
|
+
}));
|
|
604
|
+
try {
|
|
605
|
+
window.localStorage.removeItem(MV_STORAGE_KEY);
|
|
606
|
+
} catch (error) {}
|
|
607
|
+
}
|
|
608
|
+
function mvReconcile() {
|
|
609
|
+
if (mvState.reconciled) return;
|
|
610
|
+
try {
|
|
611
|
+
const list = PTX.sessions.list.getSnapshot();
|
|
612
|
+
if (list.phase !== 'ready') return;
|
|
613
|
+
const kept = mvReconcileTree(mvState.layout, (sessionId)=>{
|
|
614
|
+
const row = list.byId[sessionId];
|
|
615
|
+
if (row === undefined || row.blank === true) return null;
|
|
616
|
+
if (mvIsArchived(sessionId)) return null;
|
|
617
|
+
return sessionId;
|
|
618
|
+
});
|
|
619
|
+
const leaves = mvLeaves(kept);
|
|
620
|
+
const focused = leaves.some((leaf)=>leaf.id === mvState.focused) ? mvState.focused : leaves[0].id;
|
|
621
|
+
mvCommit((current)=>({
|
|
622
|
+
...current,
|
|
623
|
+
layout: kept,
|
|
624
|
+
focused,
|
|
625
|
+
reconciled: true
|
|
626
|
+
}));
|
|
627
|
+
} catch (error) {
|
|
628
|
+
mvCommit((current)=>({
|
|
629
|
+
...current,
|
|
630
|
+
reconciled: true
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function mvOpenForSession(sessionId) {
|
|
635
|
+
if (sessionId === null) return;
|
|
636
|
+
mvCommit((current)=>{
|
|
637
|
+
const owners = current.views.filter((view)=>mvViewKey(view.layout).split(',').includes(sessionId));
|
|
638
|
+
const owner = owners[0];
|
|
639
|
+
if (owner !== undefined) {
|
|
640
|
+
if (current.currentViewId === owner.id && mvViewKey(current.layout) === mvViewKey(owner.layout)) return current;
|
|
641
|
+
return {
|
|
642
|
+
...current,
|
|
643
|
+
layout: owner.layout,
|
|
644
|
+
currentViewId: owner.id,
|
|
645
|
+
maximized: null,
|
|
646
|
+
preset: mvLeaves(owner.layout).length
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (mvViewKey(current.layout).split(',').includes(sessionId)) {
|
|
650
|
+
return current.currentViewId === null ? {
|
|
651
|
+
...current,
|
|
652
|
+
currentViewId: null
|
|
653
|
+
} : current;
|
|
654
|
+
}
|
|
655
|
+
const layout = mvWithSession(mvPreset(2), 'p1', sessionId);
|
|
656
|
+
return {
|
|
657
|
+
...current,
|
|
658
|
+
layout,
|
|
659
|
+
currentViewId: null,
|
|
660
|
+
focused: 'p1',
|
|
661
|
+
preset: 2,
|
|
662
|
+
maximized: null
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function mvViewOwner(sessionId, views, currentViewId) {
|
|
667
|
+
for (const view of views){
|
|
668
|
+
if (view.id === currentViewId) continue;
|
|
669
|
+
if (mvViewKey(view.layout).split(',').includes(sessionId)) return view;
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
function mvSetShape(count) {
|
|
674
|
+
mvCommit((current)=>{
|
|
675
|
+
const layout = mvShapeTo(current.layout, count);
|
|
676
|
+
if (layout === current.layout) return current;
|
|
677
|
+
const leaves = mvLeaves(layout);
|
|
678
|
+
const focused = leaves.some((leaf)=>leaf.id === current.focused) ? current.focused : leaves[0].id;
|
|
679
|
+
return {
|
|
680
|
+
...current,
|
|
681
|
+
layout,
|
|
682
|
+
focused,
|
|
683
|
+
preset: count,
|
|
684
|
+
maximized: null,
|
|
685
|
+
seeded: true
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function mvBind(leafId, sessionId) {
|
|
690
|
+
mvCommit((current)=>{
|
|
691
|
+
const layout = mvWithSession(current.layout, leafId, sessionId);
|
|
692
|
+
if (layout === current.layout) return current;
|
|
693
|
+
const focused = sessionId === null ? current.focused : leafId;
|
|
694
|
+
return {
|
|
695
|
+
...current,
|
|
696
|
+
layout,
|
|
697
|
+
focused,
|
|
698
|
+
maximized: current.maximized === leafId ? null : current.maximized
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function mvFocus(leafId) {
|
|
703
|
+
mvCommit((current)=>current.focused === leafId ? current : {
|
|
704
|
+
...current,
|
|
705
|
+
focused: leafId
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function mvToggleMaximize(leafId) {
|
|
709
|
+
mvCommit((current)=>current.maximized === leafId ? {
|
|
710
|
+
...current,
|
|
711
|
+
maximized: null
|
|
712
|
+
} : {
|
|
713
|
+
...current,
|
|
714
|
+
maximized: leafId,
|
|
715
|
+
focused: leafId
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function mvRestore() {
|
|
719
|
+
mvCommit((current)=>current.maximized === null ? current : {
|
|
720
|
+
...current,
|
|
721
|
+
maximized: null
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
function mvResizeSplit(splitId, ratio) {
|
|
725
|
+
mvCommit((current)=>{
|
|
726
|
+
const layout = mvResize(current.layout, splitId, ratio);
|
|
727
|
+
return layout === current.layout ? current : {
|
|
728
|
+
...current,
|
|
729
|
+
layout
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function mvSwapLeaves(aId, bId) {
|
|
734
|
+
mvCommit((current)=>{
|
|
735
|
+
const layout = mvSwapSessions(current.layout, aId, bId);
|
|
736
|
+
return layout === current.layout ? current : {
|
|
737
|
+
...current,
|
|
738
|
+
layout,
|
|
739
|
+
maximized: null
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function mvRemovePane(leafId) {
|
|
744
|
+
mvCommit((current)=>{
|
|
745
|
+
const leaf = mvLeafById(current.layout, leafId);
|
|
746
|
+
if (leaf === null) return current;
|
|
747
|
+
if (leaf.sessionId !== null) {
|
|
748
|
+
const cleared = mvWithSession(current.layout, leafId, null);
|
|
749
|
+
return cleared === current.layout ? current : {
|
|
750
|
+
...current,
|
|
751
|
+
layout: cleared,
|
|
752
|
+
focused: leafId
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const leaves = mvLeaves(current.layout);
|
|
756
|
+
if (leaves.length <= 1) return current;
|
|
757
|
+
const layout = mvRemoveLeaf(current.layout, leafId);
|
|
758
|
+
if (layout === null) return current;
|
|
759
|
+
const nextLeaves = mvLeaves(layout);
|
|
760
|
+
const focused = nextLeaves.some((item)=>item.id === current.focused) ? current.focused : nextLeaves[0].id;
|
|
761
|
+
return {
|
|
762
|
+
...current,
|
|
763
|
+
layout,
|
|
764
|
+
focused,
|
|
765
|
+
preset: nextLeaves.length,
|
|
766
|
+
maximized: current.maximized === leafId ? null : current.maximized
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function mvApplyView(viewId) {
|
|
771
|
+
mvCommit((current)=>{
|
|
772
|
+
const view = current.views.find((item)=>item.id === viewId);
|
|
773
|
+
if (view === undefined) return current;
|
|
774
|
+
const leaves = mvLeaves(view.layout);
|
|
775
|
+
return {
|
|
776
|
+
...current,
|
|
777
|
+
layout: view.layout,
|
|
778
|
+
focused: leaves[0].id,
|
|
779
|
+
preset: leaves.length,
|
|
780
|
+
maximized: null,
|
|
781
|
+
currentViewId: view.id
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function mvStartView() {
|
|
786
|
+
mvCommit((current)=>({
|
|
787
|
+
...current,
|
|
788
|
+
layout: mvPreset(2),
|
|
789
|
+
focused: 'p1',
|
|
790
|
+
preset: 2,
|
|
791
|
+
maximized: null,
|
|
792
|
+
currentViewId: null
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
function mvDeleteView(viewId) {
|
|
796
|
+
mvCommit((current)=>{
|
|
797
|
+
if (!current.views.some((item)=>item.id === viewId)) return current;
|
|
798
|
+
return {
|
|
799
|
+
...current,
|
|
800
|
+
views: current.views.filter((item)=>item.id !== viewId),
|
|
801
|
+
currentViewId: current.currentViewId === viewId ? null : current.currentViewId
|
|
802
|
+
};
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const MV_UPDATE_KEY = 'dsh.multiview.update.v1';
|
|
807
|
+
const MV_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
808
|
+
const MV_BACKOFF_BASE = 60 * 60 * 1000;
|
|
809
|
+
const MV_BACKOFF_MAX = 12 * 60 * 60 * 1000;
|
|
810
|
+
const MV_POLL_INTERVAL = 1500;
|
|
811
|
+
const MV_POLL_LIMIT = 200;
|
|
812
|
+
const MV_REQUEST_TIMEOUT = 20000;
|
|
813
|
+
let mvUpdateState = {
|
|
814
|
+
status: 'idle',
|
|
815
|
+
installed: MV_PLUGIN_VERSION,
|
|
816
|
+
latest: null,
|
|
817
|
+
source: null,
|
|
818
|
+
available: false,
|
|
819
|
+
localOnly: false,
|
|
820
|
+
checkedAt: 0,
|
|
821
|
+
nextAt: 0,
|
|
822
|
+
failures: 0,
|
|
823
|
+
error: null,
|
|
824
|
+
operationId: null,
|
|
825
|
+
operationState: null,
|
|
826
|
+
restartRequired: false,
|
|
827
|
+
restartSupported: false
|
|
828
|
+
};
|
|
829
|
+
const mvUpdateListeners = new Set();
|
|
830
|
+
function mvUpdateGetState() {
|
|
831
|
+
return mvUpdateState;
|
|
832
|
+
}
|
|
833
|
+
function mvUpdateSubscribe(listener) {
|
|
834
|
+
mvUpdateListeners.add(listener);
|
|
835
|
+
return ()=>{
|
|
836
|
+
mvUpdateListeners.delete(listener);
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function mvUpdateCommit(patch) {
|
|
840
|
+
mvUpdateState = {
|
|
841
|
+
...mvUpdateState,
|
|
842
|
+
...patch
|
|
843
|
+
};
|
|
844
|
+
try {
|
|
845
|
+
window.localStorage.setItem(MV_UPDATE_KEY, JSON.stringify({
|
|
846
|
+
checkedAt: mvUpdateState.checkedAt,
|
|
847
|
+
nextAt: mvUpdateState.nextAt,
|
|
848
|
+
failures: mvUpdateState.failures,
|
|
849
|
+
latest: mvUpdateState.latest,
|
|
850
|
+
available: mvUpdateState.available,
|
|
851
|
+
localOnly: mvUpdateState.localOnly,
|
|
852
|
+
source: mvUpdateState.source
|
|
853
|
+
}));
|
|
854
|
+
} catch (error) {}
|
|
855
|
+
for (const listener of mvUpdateListeners){
|
|
856
|
+
try {
|
|
857
|
+
listener();
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.error('dsh-multiview: update listener failed', error);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function mvUpdateHydrate() {
|
|
864
|
+
try {
|
|
865
|
+
const raw = JSON.parse(window.localStorage.getItem(MV_UPDATE_KEY) ?? 'null');
|
|
866
|
+
if (raw === null || typeof raw !== 'object') return;
|
|
867
|
+
mvUpdateState = {
|
|
868
|
+
...mvUpdateState,
|
|
869
|
+
checkedAt: typeof raw.checkedAt === 'number' ? raw.checkedAt : 0,
|
|
870
|
+
nextAt: typeof raw.nextAt === 'number' ? raw.nextAt : 0,
|
|
871
|
+
failures: typeof raw.failures === 'number' ? raw.failures : 0,
|
|
872
|
+
latest: typeof raw.latest === 'string' ? raw.latest : null,
|
|
873
|
+
available: raw.available === true,
|
|
874
|
+
localOnly: raw.localOnly === true,
|
|
875
|
+
source: typeof raw.source === 'string' ? raw.source : null
|
|
876
|
+
};
|
|
877
|
+
} catch (error) {}
|
|
878
|
+
}
|
|
879
|
+
function mvVersionParts(version) {
|
|
880
|
+
const core = String(version).split('-')[0].split('+')[0];
|
|
881
|
+
return core.split('.').map((part)=>{
|
|
882
|
+
const value = Number.parseInt(part, 10);
|
|
883
|
+
return Number.isFinite(value) ? value : 0;
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function mvVersionGt(left, right) {
|
|
887
|
+
const a = mvVersionParts(left);
|
|
888
|
+
const b = mvVersionParts(right);
|
|
889
|
+
for(let index = 0; index < Math.max(a.length, b.length); index += 1){
|
|
890
|
+
const first = a[index] ?? 0;
|
|
891
|
+
const second = b[index] ?? 0;
|
|
892
|
+
if (first !== second) return first > second;
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
async function mvUpdateGet(url) {
|
|
897
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
898
|
+
const timer = controller === null ? 0 : setTimeout(()=>controller.abort(), MV_REQUEST_TIMEOUT);
|
|
899
|
+
try {
|
|
900
|
+
const response = await fetch(url, {
|
|
901
|
+
credentials: 'same-origin',
|
|
902
|
+
headers: {
|
|
903
|
+
accept: 'application/json'
|
|
904
|
+
},
|
|
905
|
+
signal: controller === null ? undefined : controller.signal
|
|
906
|
+
});
|
|
907
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
908
|
+
return await response.json();
|
|
909
|
+
} finally{
|
|
910
|
+
if (timer !== 0) clearTimeout(timer);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function mvUpdatePost(url, body) {
|
|
914
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
915
|
+
const timer = controller === null ? 0 : setTimeout(()=>controller.abort(), MV_REQUEST_TIMEOUT);
|
|
916
|
+
try {
|
|
917
|
+
const response = await fetch(url, {
|
|
918
|
+
method: 'POST',
|
|
919
|
+
credentials: 'same-origin',
|
|
920
|
+
headers: {
|
|
921
|
+
'content-type': 'application/json',
|
|
922
|
+
accept: 'application/json'
|
|
923
|
+
},
|
|
924
|
+
body: JSON.stringify(body),
|
|
925
|
+
signal: controller === null ? undefined : controller.signal
|
|
926
|
+
});
|
|
927
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
928
|
+
return await response.json();
|
|
929
|
+
} finally{
|
|
930
|
+
if (timer !== 0) clearTimeout(timer);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function mvUpdateCheck(force) {
|
|
934
|
+
const now = Date.now();
|
|
935
|
+
if (force !== true) {
|
|
936
|
+
if (now - mvUpdateState.checkedAt < MV_CHECK_INTERVAL) return;
|
|
937
|
+
if (mvUpdateState.nextAt > now) return;
|
|
938
|
+
}
|
|
939
|
+
if (mvUpdateState.status === 'checking' || mvUpdateState.status === 'installing') return;
|
|
940
|
+
mvUpdateCommit({
|
|
941
|
+
status: 'checking',
|
|
942
|
+
error: null
|
|
943
|
+
});
|
|
944
|
+
try {
|
|
945
|
+
let capabilities = null;
|
|
946
|
+
try {
|
|
947
|
+
capabilities = await mvUpdateGet('/dsh-market/api/v1/capabilities');
|
|
948
|
+
} catch (error) {
|
|
949
|
+
capabilities = null;
|
|
950
|
+
}
|
|
951
|
+
if (capabilities === null || capabilities.schema !== 'dsh-market/update-api/v1') {
|
|
952
|
+
mvUpdateCommit({
|
|
953
|
+
status: 'unsupported',
|
|
954
|
+
checkedAt: now,
|
|
955
|
+
nextAt: now + MV_CHECK_INTERVAL,
|
|
956
|
+
failures: 0
|
|
957
|
+
});
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const endpoints = capabilities.endpoints ?? {};
|
|
961
|
+
const checkPath = typeof endpoints.updates === 'string' ? endpoints.updates : '/dsh-market/api/v1/updates';
|
|
962
|
+
const answer = await mvUpdateGet(checkPath + '?name=dsh-multiview');
|
|
963
|
+
const pkg = answer?.package ?? null;
|
|
964
|
+
const installed = typeof pkg?.installedVersion === 'string' ? pkg.installedVersion : MV_PLUGIN_VERSION;
|
|
965
|
+
const latest = typeof pkg?.latestVersion === 'string' ? pkg.latestVersion : null;
|
|
966
|
+
const source = typeof pkg?.source === 'string' ? pkg.source : null;
|
|
967
|
+
const available = pkg?.updateAvailable === true || latest !== null && mvVersionGt(latest, installed);
|
|
968
|
+
mvUpdateCommit({
|
|
969
|
+
status: available ? 'available' : 'idle',
|
|
970
|
+
installed,
|
|
971
|
+
latest,
|
|
972
|
+
source,
|
|
973
|
+
available,
|
|
974
|
+
localOnly: source !== null && /link|local|file/i.test(source),
|
|
975
|
+
checkedAt: now,
|
|
976
|
+
nextAt: now + MV_CHECK_INTERVAL,
|
|
977
|
+
failures: 0,
|
|
978
|
+
error: null,
|
|
979
|
+
restartSupported: capabilities?.restart?.supported === true
|
|
980
|
+
});
|
|
981
|
+
} catch (error) {
|
|
982
|
+
const failures = mvUpdateState.failures + 1;
|
|
983
|
+
const delay = Math.min(MV_BACKOFF_BASE * Math.pow(2, failures - 1), MV_BACKOFF_MAX);
|
|
984
|
+
mvUpdateCommit({
|
|
985
|
+
status: 'error',
|
|
986
|
+
failures,
|
|
987
|
+
checkedAt: now,
|
|
988
|
+
nextAt: now + delay,
|
|
989
|
+
error: error instanceof Error ? error.message : String(error)
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function mvUpdateInstall() {
|
|
994
|
+
if (mvUpdateState.status === 'installing') return;
|
|
995
|
+
mvUpdateCommit({
|
|
996
|
+
status: 'installing',
|
|
997
|
+
error: null,
|
|
998
|
+
operationState: 'queued'
|
|
999
|
+
});
|
|
1000
|
+
try {
|
|
1001
|
+
let capabilities = null;
|
|
1002
|
+
try {
|
|
1003
|
+
capabilities = await mvUpdateGet('/dsh-market/api/v1/capabilities');
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
capabilities = null;
|
|
1006
|
+
}
|
|
1007
|
+
if (capabilities === null) {
|
|
1008
|
+
mvUpdateCommit({
|
|
1009
|
+
status: 'unsupported',
|
|
1010
|
+
error: null
|
|
1011
|
+
});
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const endpoints = capabilities.endpoints ?? {};
|
|
1015
|
+
const updatesPath = typeof endpoints.updates === 'string' ? endpoints.updates : '/dsh-market/api/v1/updates';
|
|
1016
|
+
const operationsPath = typeof endpoints.operations === 'string' ? endpoints.operations : '/dsh-market/api/v1/operations';
|
|
1017
|
+
const accepted = await mvUpdatePost(updatesPath, {
|
|
1018
|
+
packageName: 'dsh-multiview'
|
|
1019
|
+
});
|
|
1020
|
+
const operationId = typeof accepted?.operationId === 'string' ? accepted.operationId : null;
|
|
1021
|
+
if (operationId === null) throw new Error('market returned no operation');
|
|
1022
|
+
mvUpdateCommit({
|
|
1023
|
+
operationId,
|
|
1024
|
+
operationState: 'running'
|
|
1025
|
+
});
|
|
1026
|
+
for(let attempt = 0; attempt < MV_POLL_LIMIT; attempt += 1){
|
|
1027
|
+
await new Promise((resolve)=>setTimeout(resolve, MV_POLL_INTERVAL));
|
|
1028
|
+
const record = await mvUpdateGet(operationsPath + '?operationId=' + encodeURIComponent(operationId));
|
|
1029
|
+
const operation = record?.operation ?? record;
|
|
1030
|
+
const state = typeof operation?.state === 'string' ? operation.state : null;
|
|
1031
|
+
mvUpdateCommit({
|
|
1032
|
+
operationState: state
|
|
1033
|
+
});
|
|
1034
|
+
if (state === 'succeeded') {
|
|
1035
|
+
mvUpdateCommit({
|
|
1036
|
+
status: 'done',
|
|
1037
|
+
available: false,
|
|
1038
|
+
installed: typeof operation?.installedVersion === 'string' ? operation.installedVersion : mvUpdateState.installed,
|
|
1039
|
+
restartRequired: operation?.restartRequired === true,
|
|
1040
|
+
checkedAt: Date.now(),
|
|
1041
|
+
nextAt: Date.now() + MV_CHECK_INTERVAL
|
|
1042
|
+
});
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (state === 'failed' || state === 'cancelled' || state === 'rolled-back') {
|
|
1046
|
+
const message = operation?.message ?? operation?.failureCode ?? state;
|
|
1047
|
+
mvUpdateCommit({
|
|
1048
|
+
status: 'error',
|
|
1049
|
+
error: typeof message === 'string' ? message : String(message)
|
|
1050
|
+
});
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
mvUpdateCommit({
|
|
1055
|
+
status: 'error',
|
|
1056
|
+
error: 'operation timed out'
|
|
1057
|
+
});
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
mvUpdateCommit({
|
|
1060
|
+
status: 'error',
|
|
1061
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function mvUpdateMaybeAutoCheck() {
|
|
1066
|
+
if (mvUpdateState.status === 'checking' || mvUpdateState.status === 'installing') return;
|
|
1067
|
+
if (Date.now() - mvUpdateState.checkedAt < MV_CHECK_INTERVAL) return;
|
|
1068
|
+
if (mvUpdateState.nextAt > Date.now()) return;
|
|
1069
|
+
void mvUpdateCheck(false);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const MV_CSS = `
|
|
1073
|
+
.mv-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}
|
|
1074
|
+
.mv-bar{display:flex;align-items:center;gap:8px;height:36px;padding:0 10px 0 12px;flex:none;border-bottom:.5px solid var(--dsw-alias-border-l3)}
|
|
1075
|
+
.mv-bar-title{font-size:13px;line-height:16px;font-weight:500;color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-right:2px}
|
|
1076
|
+
.mv-bar-spacer{flex:1;min-width:0}
|
|
1077
|
+
.mv-seg{display:flex;align-items:center;gap:2px;flex:none}
|
|
1078
|
+
.mv-chip{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:24px;min-width:24px;padding:0 6px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px;cursor:pointer;white-space:nowrap}
|
|
1079
|
+
.mv-chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1080
|
+
.mv-chip[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1081
|
+
.mv-chip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
1082
|
+
.mv-body{display:flex;flex:1;min-height:0;min-width:0}
|
|
1083
|
+
.mv-split{display:flex;flex:1 1 0;min-width:0;min-height:0}
|
|
1084
|
+
.mv-split[data-dir="row"]{flex-direction:row}
|
|
1085
|
+
.mv-split[data-dir="col"]{flex-direction:column}
|
|
1086
|
+
.mv-slot{display:flex;min-width:0;min-height:0;flex-basis:0}
|
|
1087
|
+
.mv-slot[data-dir="row"]{flex-direction:column}
|
|
1088
|
+
.mv-divider{position:relative;flex:0 0 1px;background:var(--dsw-alias-border-l3);z-index:2}
|
|
1089
|
+
.mv-divider[data-dir="row"]{cursor:col-resize}
|
|
1090
|
+
.mv-divider[data-dir="col"]{cursor:row-resize}
|
|
1091
|
+
.mv-divider::after{content:"";position:absolute;background:transparent}
|
|
1092
|
+
.mv-divider[data-dir="row"]::after{top:0;bottom:0;left:-4px;right:-4px}
|
|
1093
|
+
.mv-divider[data-dir="col"]::after{left:0;right:0;top:-4px;bottom:-4px}
|
|
1094
|
+
.mv-divider:hover::before,.mv-divider[data-dragging="true"]::before{content:"";position:absolute;background:var(--dsw-alias-state-business-primary);opacity:.55;border-radius:2px}
|
|
1095
|
+
.mv-divider[data-dir="row"]:hover::before,.mv-divider[data-dir="row"][data-dragging="true"]::before{top:0;bottom:0;left:-1px;width:2px}
|
|
1096
|
+
.mv-divider[data-dir="col"]:hover::before,.mv-divider[data-dir="col"][data-dragging="true"]::before{left:0;right:0;top:-1px;height:2px}
|
|
1097
|
+
.mv-divider[data-dragging="true"]{z-index:6}
|
|
1098
|
+
.mv-pane{display:flex;flex-direction:column;flex:1 1 0;min-width:0;min-height:0;position:relative;background:var(--dsw-alias-bg-base)}
|
|
1099
|
+
.mv-head{display:flex;align-items:center;gap:8px;height:34px;padding:0 10px 0 12px;flex:none;color:var(--dsw-alias-label-tertiary);border-bottom:.5px solid transparent}
|
|
1100
|
+
.mv-pane[data-focused="true"] .mv-head{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-border-l3)}
|
|
1101
|
+
.mv-pane[data-drop="true"]::after{content:"";position:absolute;inset:0;border:1px solid var(--dsw-alias-state-business-primary);border-radius:8px;pointer-events:none;z-index:5}
|
|
1102
|
+
.mv-dot{width:6px;height:6px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary)}
|
|
1103
|
+
.mv-dot[data-state="running"]{background:var(--dsw-alias-state-business-primary)}
|
|
1104
|
+
.mv-dot[data-state="waiting"]{background:var(--dsw-alias-state-warn-primary)}
|
|
1105
|
+
.mv-dot[data-state="unread"]{background:var(--dsw-alias-state-success-primary)}
|
|
1106
|
+
.mv-head-title{flex:1;min-width:0;font-size:13px;line-height:16px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:grab}
|
|
1107
|
+
.mv-head-title:active{cursor:grabbing}
|
|
1108
|
+
.mv-head-actions{display:flex;align-items:center;gap:2px;flex:none;opacity:0}
|
|
1109
|
+
.mv-pane:hover .mv-head-actions,.mv-pane[data-focused="true"] .mv-head-actions,.mv-head-actions:focus-within{opacity:1}
|
|
1110
|
+
.mv-icon{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;border-radius:6px;background:transparent;color:inherit;cursor:pointer}
|
|
1111
|
+
.mv-icon:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1112
|
+
.mv-icon:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
1113
|
+
.mv-pane-body{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
|
|
1114
|
+
.mv-note{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;height:100%;padding:24px;font-size:13px;line-height:20px;text-align:center;color:var(--dsw-alias-label-tertiary)}
|
|
1115
|
+
.mv-note[data-error="true"]{color:var(--dsw-alias-state-error-primary)}
|
|
1116
|
+
.mv-note-action{height:28px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}
|
|
1117
|
+
.mv-note-action:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1118
|
+
.mv-picker{display:flex;flex-direction:column;flex:1;min-height:0;padding:12px}
|
|
1119
|
+
.mv-picker-search{height:30px;flex:none;margin:0 0 8px;padding:0 10px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:13px;outline:none}
|
|
1120
|
+
.mv-picker-search:focus{border-color:var(--dsw-alias-state-business-primary)}
|
|
1121
|
+
.mv-picker-list{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:2px}
|
|
1122
|
+
.mv-picker-item{display:flex;flex-direction:row;justify-content:space-between;gap:8px;align-items:center;width:100%;padding:6px 8px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;font-size:13px;line-height:18px}
|
|
1123
|
+
.mv-picker-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1124
|
+
.mv-picker-item[data-depth="1"]{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
1125
|
+
.mv-picker-item[data-depth="1"]:hover{color:var(--dsw-alias-label-primary)}
|
|
1126
|
+
.mv-picker-empty{padding:12px 8px;font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
1127
|
+
.mv-picker-group{padding:10px 8px 4px;font-size:11px;font-weight:600;letter-spacing:0.02em;color:var(--dsw-alias-label-tertiary);text-transform:none}
|
|
1128
|
+
.mv-views{position:relative;display:inline-flex}
|
|
1129
|
+
.mv-views-toggle{position:relative}
|
|
1130
|
+
.mv-views-toggle[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1131
|
+
.mv-views-count{position:absolute;top:-3px;right:-4px;min-width:12px;height:12px;padding:0 2px;border:1.5px solid var(--dsw-alias-bg-base);border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);font-size:8px;line-height:12px;text-align:center;pointer-events:none}
|
|
1132
|
+
.mv-views-pop{position:absolute;top:calc(100% + 6px);left:0;z-index:12;min-width:240px;max-width:min(520px, 64vw);max-height:320px;overflow:auto;padding:4px;border:0.5px solid var(--dsw-alias-border-l3);border-radius:8px;background:var(--dsw-alias-bg-base)}
|
|
1133
|
+
.mv-views-empty{padding:10px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
1134
|
+
.mv-views-row{display:flex;align-items:center;gap:4px;border-radius:6px}
|
|
1135
|
+
.mv-views-row[data-active="true"]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1136
|
+
.mv-views-name{flex:1;min-width:0;padding:6px 8px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
1137
|
+
.mv-views-name:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1138
|
+
.mv-views-del{flex:none;width:22px;height:22px;margin-right:4px;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;cursor:pointer}
|
|
1139
|
+
.mv-views-del:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1140
|
+
.mv-update{position:relative}
|
|
1141
|
+
.mv-update[data-has-update="true"]::after{content:"";position:absolute;top:2px;right:2px;width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-error-primary)}
|
|
1142
|
+
.mv-update[data-busy="true"] svg{animation:mv-spin 1s linear infinite}
|
|
1143
|
+
@keyframes mv-spin{to{transform:rotate(360deg)}}
|
|
1144
|
+
.mv-dialog-backdrop{position:fixed;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb, var(--dsw-alias-label-primary) 22%, transparent)}
|
|
1145
|
+
.mv-dialog{width:min(420px, calc(100vw - 48px));padding:16px;border:0.5px solid var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-base)}
|
|
1146
|
+
.mv-dialog-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
1147
|
+
.mv-dialog-text{margin-top:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);word-break:break-word}
|
|
1148
|
+
.mv-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
|
|
1149
|
+
.mv-dialog-button{height:28px;padding:0 12px;border:0.5px solid var(--dsw-alias-border-l3);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}
|
|
1150
|
+
.mv-dialog-button:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1151
|
+
.mv-dialog-button--primary{border-color:transparent;background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-bg-base)}
|
|
1152
|
+
.mv-picker-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1153
|
+
.mv-picker-owner{flex:none;max-width:45%;padding:0 6px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1154
|
+
.mv-picker-item[data-owned="true"]{opacity:0.45;cursor:not-allowed}
|
|
1155
|
+
.mv-picker-item[data-owned="true"]:hover{background:transparent}
|
|
1156
|
+
.mv-bar{color:var(--dsw-alias-label-primary)}
|
|
1157
|
+
.mv-icon{color:var(--dsw-alias-label-primary)}
|
|
1158
|
+
.mv-icon:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1159
|
+
.mv-chip{color:var(--dsw-alias-label-secondary)}
|
|
1160
|
+
.mv-chip[data-active="true"]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
|
|
1161
|
+
@media (prefers-reduced-motion: reduce){*{transition:none !important;animation:none !important}}
|
|
1162
|
+
`;
|
|
1163
|
+
function MvGlyph(props) {
|
|
1164
|
+
const { path } = props;
|
|
1165
|
+
return h('svg', {
|
|
1166
|
+
viewBox: '0 0 16 16',
|
|
1167
|
+
width: 16,
|
|
1168
|
+
height: 16,
|
|
1169
|
+
'aria-hidden': true,
|
|
1170
|
+
fill: 'none'
|
|
1171
|
+
}, h('path', {
|
|
1172
|
+
d: path,
|
|
1173
|
+
stroke: 'currentColor',
|
|
1174
|
+
strokeWidth: 1.4,
|
|
1175
|
+
strokeLinecap: 'round',
|
|
1176
|
+
strokeLinejoin: 'round'
|
|
1177
|
+
}));
|
|
1178
|
+
}
|
|
1179
|
+
function MvIconButton(props) {
|
|
1180
|
+
const { label, path, onClick } = props;
|
|
1181
|
+
return h('button', {
|
|
1182
|
+
type: 'button',
|
|
1183
|
+
className: 'mv-icon',
|
|
1184
|
+
title: label,
|
|
1185
|
+
'aria-label': label,
|
|
1186
|
+
onClick
|
|
1187
|
+
}, h(MvGlyph, {
|
|
1188
|
+
path
|
|
1189
|
+
}));
|
|
1190
|
+
}
|
|
1191
|
+
function MvNote(props) {
|
|
1192
|
+
const { error, children } = props;
|
|
1193
|
+
return h('div', {
|
|
1194
|
+
className: 'mv-note',
|
|
1195
|
+
'data-error': error === true ? 'true' : 'false'
|
|
1196
|
+
}, children);
|
|
1197
|
+
}
|
|
1198
|
+
const MV_ICON = {
|
|
1199
|
+
maximize: 'M3 6V3h3M13 10v3h-3M3 10v3h3M13 6V3h-3',
|
|
1200
|
+
restore: 'M6 6H3v3M10 10h3V7M10 6V3h3M6 10H3V7',
|
|
1201
|
+
close: 'M4 4l8 8M12 4l-8 8',
|
|
1202
|
+
external: 'M6 10L13 3M9 3h4v4',
|
|
1203
|
+
updateCheck: 'M8 2a6 6 0 1 0 0 12A6 6 0 1 0 8 2zM8 10.7V5.7M5.6 8.1 8 5.6l2.4 2.5',
|
|
1204
|
+
updateInstall: 'M3 9.5v3c0 .6.45 1 1 1h8c.55 0 1-.4 1-1v-3M8 2.6v6.2M5.4 6.4 8 9l2.6-2.6',
|
|
1205
|
+
views: 'M8 2.2 13.4 5 8 7.8 2.6 5 8 2.2M2.6 8.2 8 11l5.4-2.8M2.6 11.2 8 14l5.4-2.8'
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
function mvPhase(session, conversation, summaryBlank) {
|
|
1209
|
+
const targeted = conversation.activeTargets !== undefined && conversation.activeTargets.size > 0;
|
|
1210
|
+
const shellPhase = targeted || !session.blank && !session.awaitingFirstTurn || session.running === true ? 'active' : session.promptAttempted === true ? 'engaging' : 'blank';
|
|
1211
|
+
const subagent = session.subagent;
|
|
1212
|
+
const parentPending = subagent !== null && subagent !== undefined && subagent.address !== undefined && subagent.address.mode === 'continuable' && subagent.parentAvailable === undefined;
|
|
1213
|
+
const settling = shellPhase === 'blank' && session.openState === 'loading' && summaryBlank !== true || parentPending;
|
|
1214
|
+
const hero = shellPhase === 'blank' && (session.openState === 'open' || summaryBlank === true);
|
|
1215
|
+
return settling ? 'settling' : hero ? 'hero' : 'active';
|
|
1216
|
+
}
|
|
1217
|
+
function FixedChatView(props) {
|
|
1218
|
+
return h(React.Fragment, null, props.renderSlot('conversation.session', {
|
|
1219
|
+
view: 'chat'
|
|
1220
|
+
}));
|
|
1221
|
+
}
|
|
1222
|
+
function PaneConversation(props) {
|
|
1223
|
+
const { sessionId, useSession, useConversation, useSessions, renderFactorySlot } = props;
|
|
1224
|
+
const session = useSession((value)=>value);
|
|
1225
|
+
const conversation = useConversation((value)=>value);
|
|
1226
|
+
const summaryBlank = useSessions((state)=>sessionId === undefined ? undefined : state.byId[sessionId] === undefined ? undefined : state.byId[sessionId].blank);
|
|
1227
|
+
if (session === undefined || conversation === undefined) {
|
|
1228
|
+
return h(MvNote, null, T('loading'));
|
|
1229
|
+
}
|
|
1230
|
+
const phase = mvPhase(session, conversation, summaryBlank);
|
|
1231
|
+
const hero = phase === 'hero';
|
|
1232
|
+
return renderFactorySlot('conversation.content', {
|
|
1233
|
+
variant: 'embedded',
|
|
1234
|
+
phase,
|
|
1235
|
+
hero
|
|
1236
|
+
}, {
|
|
1237
|
+
slots: {
|
|
1238
|
+
views: FixedChatView
|
|
1239
|
+
},
|
|
1240
|
+
fallback: h(MvNote, {
|
|
1241
|
+
error: true
|
|
1242
|
+
}, T('noContent'))
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
class MvPaneBoundary extends React.Component {
|
|
1246
|
+
constructor(props){
|
|
1247
|
+
super(props);
|
|
1248
|
+
this.state = {
|
|
1249
|
+
failed: false,
|
|
1250
|
+
message: ''
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
static getDerivedStateFromError(error) {
|
|
1254
|
+
return {
|
|
1255
|
+
failed: true,
|
|
1256
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
componentDidCatch(error, info) {
|
|
1260
|
+
console.error('dsh-multiview: pane crashed', error, info === undefined ? '' : info.componentStack);
|
|
1261
|
+
}
|
|
1262
|
+
render() {
|
|
1263
|
+
if (this.state.failed) {
|
|
1264
|
+
return h(MvNote, {
|
|
1265
|
+
error: true
|
|
1266
|
+
}, mvFormat(T('paneCrashed'), {
|
|
1267
|
+
message: this.state.message
|
|
1268
|
+
}));
|
|
1269
|
+
}
|
|
1270
|
+
return this.props.children;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function PaneHost(props) {
|
|
1274
|
+
const { sessionId, SessionProvider, renderSlot } = props;
|
|
1275
|
+
const [state, setState] = React.useState({
|
|
1276
|
+
reference: null,
|
|
1277
|
+
error: null
|
|
1278
|
+
});
|
|
1279
|
+
React.useEffect(()=>{
|
|
1280
|
+
if (sessionId === null || sessionId === undefined) {
|
|
1281
|
+
setState({
|
|
1282
|
+
reference: null,
|
|
1283
|
+
error: null
|
|
1284
|
+
});
|
|
1285
|
+
return undefined;
|
|
1286
|
+
}
|
|
1287
|
+
let reference = null;
|
|
1288
|
+
try {
|
|
1289
|
+
reference = PTX.sessions.retain(sessionId, {
|
|
1290
|
+
source: 'multiview'
|
|
1291
|
+
});
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
setState({
|
|
1294
|
+
reference: null,
|
|
1295
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1296
|
+
});
|
|
1297
|
+
return undefined;
|
|
1298
|
+
}
|
|
1299
|
+
let cancelled = false;
|
|
1300
|
+
setState({
|
|
1301
|
+
reference,
|
|
1302
|
+
error: null
|
|
1303
|
+
});
|
|
1304
|
+
reference.ready.then(()=>{
|
|
1305
|
+
if (!cancelled) setState({
|
|
1306
|
+
reference,
|
|
1307
|
+
error: null
|
|
1308
|
+
});
|
|
1309
|
+
}, (error)=>{
|
|
1310
|
+
if (!cancelled) setState({
|
|
1311
|
+
reference: null,
|
|
1312
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
return ()=>{
|
|
1316
|
+
cancelled = true;
|
|
1317
|
+
try {
|
|
1318
|
+
reference.release();
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
console.error('dsh-multiview: releasing a Session reference failed', error);
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}, [
|
|
1324
|
+
sessionId
|
|
1325
|
+
]);
|
|
1326
|
+
if (sessionId === null || sessionId === undefined) {
|
|
1327
|
+
return h(MvNote, null, T('empty'));
|
|
1328
|
+
}
|
|
1329
|
+
if (state.error !== null) {
|
|
1330
|
+
return h(MvNote, {
|
|
1331
|
+
error: true
|
|
1332
|
+
}, mvFormat(T('openFailed'), {
|
|
1333
|
+
message: state.error
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
if (state.reference === null) {
|
|
1337
|
+
return h(MvNote, null, T('loading'));
|
|
1338
|
+
}
|
|
1339
|
+
return h(SessionProvider, {
|
|
1340
|
+
session: state.reference
|
|
1341
|
+
}, h(MvPaneBoundary, null, renderSlot(MV_PANE_SLOT, {})));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function useMvState() {
|
|
1345
|
+
return React.useSyncExternalStore(mvSubscribe, mvGetState, mvGetState);
|
|
1346
|
+
}
|
|
1347
|
+
function MultiviewPanelIcon() {
|
|
1348
|
+
return h('svg', {
|
|
1349
|
+
viewBox: '0 0 16 16',
|
|
1350
|
+
width: 16,
|
|
1351
|
+
height: 16,
|
|
1352
|
+
'aria-hidden': true,
|
|
1353
|
+
fill: 'none'
|
|
1354
|
+
}, h('rect', {
|
|
1355
|
+
x: 1.75,
|
|
1356
|
+
y: 2.75,
|
|
1357
|
+
width: 12.5,
|
|
1358
|
+
height: 10.5,
|
|
1359
|
+
rx: 2.5,
|
|
1360
|
+
stroke: 'currentColor',
|
|
1361
|
+
strokeWidth: 1.4
|
|
1362
|
+
}), h('path', {
|
|
1363
|
+
d: 'M8 2.75v10.5',
|
|
1364
|
+
stroke: 'currentColor',
|
|
1365
|
+
strokeWidth: 1.2
|
|
1366
|
+
}));
|
|
1367
|
+
}
|
|
1368
|
+
function mvDotState(status) {
|
|
1369
|
+
if (status === undefined || status === null) return 'idle';
|
|
1370
|
+
if (status.pendingInteraction !== undefined) return 'waiting';
|
|
1371
|
+
if (status.running === true) return 'running';
|
|
1372
|
+
if (status.completionUnread === true) return 'unread';
|
|
1373
|
+
return 'idle';
|
|
1374
|
+
}
|
|
1375
|
+
function mvDotLabel(state) {
|
|
1376
|
+
if (state === 'running') return T('running');
|
|
1377
|
+
if (state === 'waiting') return T('waiting');
|
|
1378
|
+
if (state === 'unread') return T('unread');
|
|
1379
|
+
return '';
|
|
1380
|
+
}
|
|
1381
|
+
function mvPickableRows(query, views = [], currentViewId = null) {
|
|
1382
|
+
const need = query.trim().toLowerCase();
|
|
1383
|
+
try {
|
|
1384
|
+
const list = PTX.sessions.list.getSnapshot();
|
|
1385
|
+
const rows = new Map();
|
|
1386
|
+
for (const id of list.ids){
|
|
1387
|
+
const row = list.byId[id];
|
|
1388
|
+
if (row === undefined || row.blank === true) continue;
|
|
1389
|
+
if (mvIsArchived(id)) continue;
|
|
1390
|
+
rows.set(id, row);
|
|
1391
|
+
}
|
|
1392
|
+
const snapshot = PTX.workspaces.list.getSnapshot();
|
|
1393
|
+
const items = Array.isArray(snapshot?.items) ? snapshot.items : [];
|
|
1394
|
+
const workspaceOf = new Map();
|
|
1395
|
+
for (const item of items){
|
|
1396
|
+
for (const id of item.sessionIds ?? [])workspaceOf.set(id, item.workspaceId);
|
|
1397
|
+
}
|
|
1398
|
+
const groups = [];
|
|
1399
|
+
for (const item of items){
|
|
1400
|
+
const ids = (item.sessionIds ?? []).filter((id)=>rows.has(id));
|
|
1401
|
+
if (ids.length === 0) continue;
|
|
1402
|
+
const label = item.title ?? String(item.path ?? '').split(/[\\/]/).pop() ?? item.workspaceId;
|
|
1403
|
+
groups.push({
|
|
1404
|
+
key: String(item.workspaceId),
|
|
1405
|
+
label: String(label),
|
|
1406
|
+
ids
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
const loose = [
|
|
1410
|
+
...rows.keys()
|
|
1411
|
+
].filter((id)=>!workspaceOf.has(id));
|
|
1412
|
+
if (loose.length > 0) groups.push({
|
|
1413
|
+
key: '__loose__',
|
|
1414
|
+
label: '',
|
|
1415
|
+
ids: loose
|
|
1416
|
+
});
|
|
1417
|
+
if (groups.length === 0) return [];
|
|
1418
|
+
const rank = (id)=>{
|
|
1419
|
+
const value = rows.get(id)?.updatedAt;
|
|
1420
|
+
return typeof value === 'number' ? value : 0;
|
|
1421
|
+
};
|
|
1422
|
+
const out = [];
|
|
1423
|
+
const multiple = groups.filter((group)=>group.ids.length > 0 && group.label !== '').length > 1;
|
|
1424
|
+
for (const group of groups){
|
|
1425
|
+
const scoped = new Set(group.ids);
|
|
1426
|
+
const children = new Map();
|
|
1427
|
+
const roots = [];
|
|
1428
|
+
const orphan = new Set();
|
|
1429
|
+
for (const id of group.ids){
|
|
1430
|
+
const parentId = rows.get(id)?.parentId;
|
|
1431
|
+
if (typeof parentId !== 'string' || parentId === id) {
|
|
1432
|
+
roots.push(id);
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (scoped.has(parentId)) {
|
|
1436
|
+
const bucket = children.get(parentId);
|
|
1437
|
+
if (bucket === undefined) children.set(parentId, [
|
|
1438
|
+
id
|
|
1439
|
+
]);
|
|
1440
|
+
else bucket.push(id);
|
|
1441
|
+
} else {
|
|
1442
|
+
orphan.add(id);
|
|
1443
|
+
roots.push(id);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const byRank = (left, right)=>rank(right) - rank(left);
|
|
1447
|
+
roots.sort(byRank);
|
|
1448
|
+
for (const bucket of children.values())bucket.sort(byRank);
|
|
1449
|
+
const before = out.length;
|
|
1450
|
+
const append = (id, depth)=>{
|
|
1451
|
+
const row = rows.get(id);
|
|
1452
|
+
if (row === undefined) return;
|
|
1453
|
+
const raw = row.title === undefined || row.title === '' ? id : row.title;
|
|
1454
|
+
const shown = orphan.has(id) ? Math.max(depth, 1) : depth;
|
|
1455
|
+
const title = shown > 0 ? '↳ ' + raw : raw;
|
|
1456
|
+
const matched = need === '' || raw.toLowerCase().includes(need) || id.toLowerCase().includes(need);
|
|
1457
|
+
const owner = mvViewOwner(id, views, currentViewId);
|
|
1458
|
+
if (matched) out.push({
|
|
1459
|
+
id,
|
|
1460
|
+
title,
|
|
1461
|
+
depth: shown,
|
|
1462
|
+
owner: owner === null ? null : owner.name
|
|
1463
|
+
});
|
|
1464
|
+
for (const childId of children.get(id) ?? [])append(childId, Math.min(shown + 1, 3));
|
|
1465
|
+
};
|
|
1466
|
+
for (const id of roots)append(id, 0);
|
|
1467
|
+
if (multiple && out.length > before && group.label !== '') {
|
|
1468
|
+
out.splice(before, 0, {
|
|
1469
|
+
kind: 'group',
|
|
1470
|
+
id: group.key,
|
|
1471
|
+
title: group.label
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return out.slice(0, 240);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
return [];
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function MvPanePicker(props) {
|
|
1481
|
+
const { onPick, views, currentViewId } = props;
|
|
1482
|
+
const [query, setQuery] = React.useState('');
|
|
1483
|
+
const rows = mvPickableRows(query, views ?? [], currentViewId ?? null);
|
|
1484
|
+
return h('div', {
|
|
1485
|
+
className: 'mv-picker',
|
|
1486
|
+
'data-mv-picker': ''
|
|
1487
|
+
}, h('input', {
|
|
1488
|
+
className: 'mv-picker-search',
|
|
1489
|
+
type: 'search',
|
|
1490
|
+
value: query,
|
|
1491
|
+
placeholder: T('pickSearch'),
|
|
1492
|
+
'aria-label': T('pickSearch'),
|
|
1493
|
+
autoFocus: true,
|
|
1494
|
+
onChange: (event)=>setQuery(event.target.value),
|
|
1495
|
+
onKeyDown: (event)=>{
|
|
1496
|
+
if (event.key === 'Enter' && rows.length > 0) onPick(rows[0].id);
|
|
1497
|
+
if (event.key === 'Escape') event.currentTarget.blur();
|
|
1498
|
+
}
|
|
1499
|
+
}), h('div', {
|
|
1500
|
+
className: 'mv-picker-list'
|
|
1501
|
+
}, rows.length === 0 ? h('div', {
|
|
1502
|
+
className: 'mv-picker-empty'
|
|
1503
|
+
}, T('pickEmpty')) : rows.map((row)=>row.kind === 'group' ? h('div', {
|
|
1504
|
+
key: 'group-' + row.id,
|
|
1505
|
+
className: 'mv-picker-group',
|
|
1506
|
+
role: 'presentation'
|
|
1507
|
+
}, row.title) : h('button', {
|
|
1508
|
+
key: row.id,
|
|
1509
|
+
type: 'button',
|
|
1510
|
+
className: 'mv-picker-item',
|
|
1511
|
+
'data-depth': String(row.depth ?? 0),
|
|
1512
|
+
'data-owned': row.owner === null || row.owner === undefined ? undefined : 'true',
|
|
1513
|
+
disabled: row.owner !== null && row.owner !== undefined,
|
|
1514
|
+
style: row.depth === 0 ? undefined : {
|
|
1515
|
+
paddingLeft: String(8 + row.depth * 14) + 'px'
|
|
1516
|
+
},
|
|
1517
|
+
title: row.owner === null || row.owner === undefined ? row.title : row.title + ' · ' + T('views') + ': ' + row.owner,
|
|
1518
|
+
onClick: ()=>onPick(row.id)
|
|
1519
|
+
}, h('span', {
|
|
1520
|
+
className: 'mv-picker-title'
|
|
1521
|
+
}, row.title), row.owner === null || row.owner === undefined ? null : h('span', {
|
|
1522
|
+
className: 'mv-picker-owner'
|
|
1523
|
+
}, row.owner)))));
|
|
1524
|
+
}
|
|
1525
|
+
function mvOpenFileInNative(sessionId, label) {
|
|
1526
|
+
try {
|
|
1527
|
+
PTX.uiWorkspace.openSession(sessionId);
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
console.error('dsh-multiview: switching to the native view failed', error);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const attempt = (remaining)=>{
|
|
1533
|
+
if (remaining <= 0) return;
|
|
1534
|
+
const chips = document.querySelectorAll('button[class*="fileLink"], a[class*="fileLink"]');
|
|
1535
|
+
for (const chip of chips){
|
|
1536
|
+
if (chip.closest('[data-multiview]') !== null) continue;
|
|
1537
|
+
if ((chip.textContent ?? '').trim() !== label) continue;
|
|
1538
|
+
chip.click();
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
setTimeout(()=>attempt(remaining - 1), 120);
|
|
1542
|
+
};
|
|
1543
|
+
setTimeout(()=>attempt(14), 140);
|
|
1544
|
+
}
|
|
1545
|
+
function MvDivider(props) {
|
|
1546
|
+
const { node, onResize } = props;
|
|
1547
|
+
const [dragging, setDragging] = React.useState(false);
|
|
1548
|
+
const ref = React.useRef(null);
|
|
1549
|
+
const lastRatio = React.useRef(node.ratio);
|
|
1550
|
+
const ratioAt = (dir, event)=>{
|
|
1551
|
+
const element = ref.current;
|
|
1552
|
+
if (element === null) return lastRatio.current;
|
|
1553
|
+
const parent = element.parentElement;
|
|
1554
|
+
if (parent === null) return lastRatio.current;
|
|
1555
|
+
const rect = parent.getBoundingClientRect();
|
|
1556
|
+
const size = dir === 'row' ? rect.width : rect.height;
|
|
1557
|
+
if (size === 0) return lastRatio.current;
|
|
1558
|
+
const offset = dir === 'row' ? event.clientX - rect.left : event.clientY - rect.top;
|
|
1559
|
+
return offset / size;
|
|
1560
|
+
};
|
|
1561
|
+
const beginDrag = (event)=>{
|
|
1562
|
+
if (event.button !== 0) return;
|
|
1563
|
+
event.preventDefault();
|
|
1564
|
+
setDragging(true);
|
|
1565
|
+
const dir = node.dir;
|
|
1566
|
+
const splitId = node.id;
|
|
1567
|
+
const divider = ref.current;
|
|
1568
|
+
const first = divider === null ? null : divider.previousElementSibling;
|
|
1569
|
+
const second = divider === null ? null : divider.nextElementSibling;
|
|
1570
|
+
let latest = node.ratio;
|
|
1571
|
+
const move = (moveEvent)=>{
|
|
1572
|
+
latest = mvClampRatio(ratioAt(dir, moveEvent));
|
|
1573
|
+
lastRatio.current = latest;
|
|
1574
|
+
if (first !== null) first.style.flexGrow = String(latest);
|
|
1575
|
+
if (second !== null) second.style.flexGrow = String(1 - latest);
|
|
1576
|
+
};
|
|
1577
|
+
const stop = ()=>{
|
|
1578
|
+
window.removeEventListener('pointermove', move);
|
|
1579
|
+
window.removeEventListener('pointerup', stop);
|
|
1580
|
+
window.removeEventListener('pointercancel', stop);
|
|
1581
|
+
setDragging(false);
|
|
1582
|
+
if (latest !== node.ratio) onResize(splitId, latest);
|
|
1583
|
+
};
|
|
1584
|
+
window.addEventListener('pointermove', move);
|
|
1585
|
+
window.addEventListener('pointerup', stop);
|
|
1586
|
+
window.addEventListener('pointercancel', stop);
|
|
1587
|
+
};
|
|
1588
|
+
return h('div', {
|
|
1589
|
+
ref,
|
|
1590
|
+
className: 'mv-divider',
|
|
1591
|
+
'data-dir': node.dir,
|
|
1592
|
+
'data-divider': node.id,
|
|
1593
|
+
'data-dragging': dragging ? 'true' : 'false',
|
|
1594
|
+
role: 'separator',
|
|
1595
|
+
'aria-orientation': node.dir === 'row' ? 'vertical' : 'horizontal',
|
|
1596
|
+
onPointerDown: beginDrag
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
function MvSplitView(props) {
|
|
1600
|
+
const { node, surface } = props;
|
|
1601
|
+
return h('div', {
|
|
1602
|
+
className: 'mv-split',
|
|
1603
|
+
'data-dir': node.dir
|
|
1604
|
+
}, h('div', {
|
|
1605
|
+
className: 'mv-slot',
|
|
1606
|
+
'data-dir': node.dir,
|
|
1607
|
+
style: {
|
|
1608
|
+
flexGrow: node.ratio
|
|
1609
|
+
}
|
|
1610
|
+
}, renderMvNode(node.a, surface)), h(MvDivider, {
|
|
1611
|
+
node,
|
|
1612
|
+
onResize: surface.onResize
|
|
1613
|
+
}), h('div', {
|
|
1614
|
+
className: 'mv-slot',
|
|
1615
|
+
'data-dir': node.dir,
|
|
1616
|
+
style: {
|
|
1617
|
+
flexGrow: 1 - node.ratio
|
|
1618
|
+
}
|
|
1619
|
+
}, renderMvNode(node.b, surface)));
|
|
1620
|
+
}
|
|
1621
|
+
function PaneFrame(props) {
|
|
1622
|
+
const { leaf, surface } = props;
|
|
1623
|
+
const { state, dragging } = surface;
|
|
1624
|
+
const focused = state.focused === leaf.id;
|
|
1625
|
+
const sessionId = leaf.sessionId;
|
|
1626
|
+
const title = sessionId === null ? T('empty') : mvTitle(sessionId) ?? T('empty');
|
|
1627
|
+
const status = sessionId === null ? undefined : surface.statuses.get(sessionId);
|
|
1628
|
+
const dot = mvDotState(status);
|
|
1629
|
+
const maximized = state.maximized === leaf.id;
|
|
1630
|
+
return h('div', {
|
|
1631
|
+
className: 'mv-pane',
|
|
1632
|
+
'data-pane-id': leaf.id,
|
|
1633
|
+
'data-pane': '',
|
|
1634
|
+
'data-focused': focused ? 'true' : 'false',
|
|
1635
|
+
'data-drop': dragging.over === leaf.id && dragging.from !== null && dragging.from !== leaf.id ? 'true' : 'false',
|
|
1636
|
+
onPointerDownCapture: ()=>surface.onFocus(leaf.id),
|
|
1637
|
+
onFocusCapture: ()=>surface.onFocus(leaf.id)
|
|
1638
|
+
}, h('div', {
|
|
1639
|
+
className: 'mv-head',
|
|
1640
|
+
'data-pane-head': ''
|
|
1641
|
+
}, h('span', {
|
|
1642
|
+
className: 'mv-dot',
|
|
1643
|
+
'data-state': dot,
|
|
1644
|
+
title: mvDotLabel(dot),
|
|
1645
|
+
'aria-hidden': dot === 'idle'
|
|
1646
|
+
}), h('span', {
|
|
1647
|
+
className: 'mv-head-title',
|
|
1648
|
+
title: sessionId === null ? T('empty') : title,
|
|
1649
|
+
onPointerDown: (event)=>surface.onHeadPointerDown(leaf.id, event)
|
|
1650
|
+
}, title), h('div', {
|
|
1651
|
+
className: 'mv-head-actions'
|
|
1652
|
+
}, h(MvIconButton, {
|
|
1653
|
+
label: maximized ? T('restore') : T('maximize'),
|
|
1654
|
+
path: maximized ? MV_ICON.restore : MV_ICON.maximize,
|
|
1655
|
+
onClick: ()=>surface.onMaximize(leaf.id)
|
|
1656
|
+
}), sessionId === null ? null : h(MvIconButton, {
|
|
1657
|
+
label: T('openMain'),
|
|
1658
|
+
path: MV_ICON.external,
|
|
1659
|
+
onClick: ()=>surface.onOpenMain(sessionId)
|
|
1660
|
+
}), h(MvIconButton, {
|
|
1661
|
+
label: T('remove'),
|
|
1662
|
+
path: MV_ICON.close,
|
|
1663
|
+
onClick: ()=>surface.onRemove(leaf.id)
|
|
1664
|
+
}))), h('div', {
|
|
1665
|
+
className: 'mv-pane-body'
|
|
1666
|
+
}, sessionId === null ? h(MvPanePicker, {
|
|
1667
|
+
onPick: (id)=>surface.onBind(leaf.id, id),
|
|
1668
|
+
views: surface.state.views,
|
|
1669
|
+
currentViewId: surface.state.currentViewId
|
|
1670
|
+
}) : h(MvPaneHost, {
|
|
1671
|
+
sessionId,
|
|
1672
|
+
SessionProvider: surface.SessionProvider,
|
|
1673
|
+
renderSlot: surface.renderSlot
|
|
1674
|
+
})));
|
|
1675
|
+
}
|
|
1676
|
+
const MvPaneHost = React.memo(function MvPaneHost(props) {
|
|
1677
|
+
return h(PaneHost, {
|
|
1678
|
+
sessionId: props.sessionId,
|
|
1679
|
+
SessionProvider: props.SessionProvider,
|
|
1680
|
+
renderSlot: props.renderSlot
|
|
1681
|
+
});
|
|
1682
|
+
}, (previous, next)=>previous.sessionId === next.sessionId && previous.SessionProvider === next.SessionProvider && previous.renderSlot === next.renderSlot);
|
|
1683
|
+
function renderMvNode(node, surface) {
|
|
1684
|
+
if (node.kind === 'leaf') return h(PaneFrame, {
|
|
1685
|
+
key: node.id,
|
|
1686
|
+
leaf: node,
|
|
1687
|
+
surface
|
|
1688
|
+
});
|
|
1689
|
+
return h(MvSplitView, {
|
|
1690
|
+
key: node.id,
|
|
1691
|
+
node,
|
|
1692
|
+
surface
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
class MvStageBoundary extends React.Component {
|
|
1696
|
+
constructor(props){
|
|
1697
|
+
super(props);
|
|
1698
|
+
this.state = {
|
|
1699
|
+
failed: false,
|
|
1700
|
+
message: ''
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
static getDerivedStateFromError(error) {
|
|
1704
|
+
return {
|
|
1705
|
+
failed: true,
|
|
1706
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
componentDidCatch(error, info) {
|
|
1710
|
+
console.error('dsh-multiview: stage crashed', error, info === undefined ? '' : info.componentStack);
|
|
1711
|
+
}
|
|
1712
|
+
render() {
|
|
1713
|
+
if (this.state.failed) {
|
|
1714
|
+
return h('div', {
|
|
1715
|
+
className: 'mv-root'
|
|
1716
|
+
}, h(MvNote, {
|
|
1717
|
+
error: true
|
|
1718
|
+
}, mvFormat(T('stageCrashed'), {
|
|
1719
|
+
message: this.state.message
|
|
1720
|
+
})));
|
|
1721
|
+
}
|
|
1722
|
+
return this.props.children;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function useMvUpdate() {
|
|
1726
|
+
return React.useSyncExternalStore(mvUpdateSubscribe, mvUpdateGetState, mvUpdateGetState);
|
|
1727
|
+
}
|
|
1728
|
+
function mvUpdateCaption(update) {
|
|
1729
|
+
if (update.status === 'checking') return T('updateChecking');
|
|
1730
|
+
if (update.status === 'installing') return T('updateInstalling');
|
|
1731
|
+
if (update.status === 'done') return T('updateDone');
|
|
1732
|
+
if (update.status === 'unsupported') return T('updateUnsupported');
|
|
1733
|
+
if (update.status === 'error') return mvFormat(T('updateFailed'), {
|
|
1734
|
+
message: update.error ?? ''
|
|
1735
|
+
});
|
|
1736
|
+
if (update.available === true) return mvFormat(T('updateAvailable'), {
|
|
1737
|
+
version: update.latest ?? ''
|
|
1738
|
+
});
|
|
1739
|
+
if (update.localOnly === true) return T('updateLocal');
|
|
1740
|
+
if (update.checkedAt > 0) return mvFormat(T('updateLatest'), {
|
|
1741
|
+
version: update.installed ?? MV_PLUGIN_VERSION
|
|
1742
|
+
});
|
|
1743
|
+
return T('updateIdle');
|
|
1744
|
+
}
|
|
1745
|
+
function MvUpdateButton() {
|
|
1746
|
+
const update = useMvUpdate();
|
|
1747
|
+
const [open, setOpen] = React.useState(false);
|
|
1748
|
+
const caption = mvUpdateCaption(update);
|
|
1749
|
+
return h(React.Fragment, null, h('button', {
|
|
1750
|
+
type: 'button',
|
|
1751
|
+
className: 'mv-icon mv-update',
|
|
1752
|
+
'data-has-update': update.available === true ? 'true' : 'false',
|
|
1753
|
+
'data-busy': update.status === 'checking' || update.status === 'installing' ? 'true' : 'false',
|
|
1754
|
+
title: caption,
|
|
1755
|
+
'aria-label': T('updateCheck'),
|
|
1756
|
+
onClick: ()=>{
|
|
1757
|
+
setOpen(true);
|
|
1758
|
+
if (update.available !== true) void mvUpdateCheck(true);
|
|
1759
|
+
}
|
|
1760
|
+
}, h(MvGlyph, {
|
|
1761
|
+
path: update.available === true ? MV_ICON.updateInstall : MV_ICON.updateCheck
|
|
1762
|
+
})), open ? h(MvUpdateDialog, {
|
|
1763
|
+
update,
|
|
1764
|
+
caption,
|
|
1765
|
+
onClose: ()=>setOpen(false)
|
|
1766
|
+
}) : null);
|
|
1767
|
+
}
|
|
1768
|
+
function MvUpdateDialog(props) {
|
|
1769
|
+
const { update, caption, onClose } = props;
|
|
1770
|
+
const busy = update.status === 'installing';
|
|
1771
|
+
const canInstall = update.available === true && !busy && update.localOnly !== true;
|
|
1772
|
+
const body = h('div', {
|
|
1773
|
+
className: 'mv-dialog-body'
|
|
1774
|
+
}, h('div', {
|
|
1775
|
+
className: 'mv-dialog-title'
|
|
1776
|
+
}, T('updateCheck')), h('div', {
|
|
1777
|
+
className: 'mv-dialog-text'
|
|
1778
|
+
}, caption), update.status === 'installing' && update.operationState !== null ? h('div', {
|
|
1779
|
+
className: 'mv-dialog-text'
|
|
1780
|
+
}, String(update.operationState)) : null, update.status === 'done' && update.restartRequired === true && update.restartSupported === true ? h('div', {
|
|
1781
|
+
className: 'mv-dialog-text'
|
|
1782
|
+
}, T('updateDone')) : null);
|
|
1783
|
+
const actions = h('div', {
|
|
1784
|
+
className: 'mv-dialog-actions'
|
|
1785
|
+
}, h('button', {
|
|
1786
|
+
type: 'button',
|
|
1787
|
+
className: 'mv-dialog-button',
|
|
1788
|
+
onClick: onClose
|
|
1789
|
+
}, T('cancel')), canInstall ? h('button', {
|
|
1790
|
+
type: 'button',
|
|
1791
|
+
className: 'mv-dialog-button mv-dialog-button--primary',
|
|
1792
|
+
onClick: ()=>void mvUpdateInstall()
|
|
1793
|
+
}, T('updateInstall')) : update.available !== true && update.status !== 'installing' ? h('button', {
|
|
1794
|
+
type: 'button',
|
|
1795
|
+
className: 'mv-dialog-button',
|
|
1796
|
+
onClick: ()=>void mvUpdateCheck(true)
|
|
1797
|
+
}, T('updateCheck')) : null);
|
|
1798
|
+
return h('div', {
|
|
1799
|
+
className: 'mv-dialog-backdrop',
|
|
1800
|
+
onClick: (event)=>{
|
|
1801
|
+
if (event.target === event.currentTarget) onClose();
|
|
1802
|
+
}
|
|
1803
|
+
}, h('div', {
|
|
1804
|
+
className: 'mv-dialog',
|
|
1805
|
+
role: 'dialog',
|
|
1806
|
+
'aria-label': T('updateCheck')
|
|
1807
|
+
}, body, actions));
|
|
1808
|
+
}
|
|
1809
|
+
function MvViewsButton(props) {
|
|
1810
|
+
const { views, currentKey } = props;
|
|
1811
|
+
const [open, setOpen] = React.useState(false);
|
|
1812
|
+
return h('div', {
|
|
1813
|
+
className: 'mv-views'
|
|
1814
|
+
}, h('button', {
|
|
1815
|
+
type: 'button',
|
|
1816
|
+
className: 'mv-icon mv-views-toggle',
|
|
1817
|
+
'data-active': open ? 'true' : 'false',
|
|
1818
|
+
'data-count': views.length > 0 ? String(views.length) : undefined,
|
|
1819
|
+
title: T('views'),
|
|
1820
|
+
'aria-label': T('views'),
|
|
1821
|
+
onClick: ()=>setOpen((value)=>!value)
|
|
1822
|
+
}, h(MvGlyph, {
|
|
1823
|
+
path: MV_ICON.views
|
|
1824
|
+
}), views.length > 0 ? h('span', {
|
|
1825
|
+
className: 'mv-views-count'
|
|
1826
|
+
}, String(views.length)) : null), open ? h('div', {
|
|
1827
|
+
className: 'mv-views-pop',
|
|
1828
|
+
role: 'menu',
|
|
1829
|
+
'aria-label': T('views')
|
|
1830
|
+
}, views.length === 0 ? h('div', {
|
|
1831
|
+
className: 'mv-views-empty'
|
|
1832
|
+
}, T('viewsEmpty')) : views.map((view)=>h('div', {
|
|
1833
|
+
key: view.id,
|
|
1834
|
+
className: 'mv-views-row',
|
|
1835
|
+
'data-active': view.id === currentKey ? 'true' : 'false'
|
|
1836
|
+
}, h('button', {
|
|
1837
|
+
type: 'button',
|
|
1838
|
+
className: 'mv-views-name',
|
|
1839
|
+
title: T('viewsHint'),
|
|
1840
|
+
onClick: ()=>{
|
|
1841
|
+
mvApplyView(view.id);
|
|
1842
|
+
setOpen(false);
|
|
1843
|
+
}
|
|
1844
|
+
}, view.name), h('button', {
|
|
1845
|
+
type: 'button',
|
|
1846
|
+
className: 'mv-views-del',
|
|
1847
|
+
title: T('viewsDelete'),
|
|
1848
|
+
'aria-label': T('viewsDelete'),
|
|
1849
|
+
onClick: (event)=>{
|
|
1850
|
+
event.stopPropagation();
|
|
1851
|
+
mvDeleteView(view.id);
|
|
1852
|
+
}
|
|
1853
|
+
}, '✕')))) : null);
|
|
1854
|
+
}
|
|
1855
|
+
function MultiviewStage(props) {
|
|
1856
|
+
const { renderSlot, SessionProvider, useSessions, useSessionStatus } = props;
|
|
1857
|
+
const list = useSessions((state)=>state);
|
|
1858
|
+
const statuses = useSessionStatus((state)=>state);
|
|
1859
|
+
const state = useMvState();
|
|
1860
|
+
const rootRef = React.useRef(null);
|
|
1861
|
+
const [narrow, setNarrow] = React.useState(false);
|
|
1862
|
+
const [drag, setDrag] = React.useState({
|
|
1863
|
+
from: null,
|
|
1864
|
+
over: null
|
|
1865
|
+
});
|
|
1866
|
+
const dragRef = React.useRef({
|
|
1867
|
+
from: null,
|
|
1868
|
+
over: null,
|
|
1869
|
+
startX: 0,
|
|
1870
|
+
startY: 0,
|
|
1871
|
+
active: false
|
|
1872
|
+
});
|
|
1873
|
+
const openedRef = React.useRef(false);
|
|
1874
|
+
React.useEffect(()=>{
|
|
1875
|
+
mvReconcile();
|
|
1876
|
+
if (openedRef.current) return;
|
|
1877
|
+
try {
|
|
1878
|
+
const snapshot = PTX.sessions.list.getSnapshot();
|
|
1879
|
+
if (snapshot.phase !== 'ready') return;
|
|
1880
|
+
const current = snapshot.ids.find((id)=>(snapshot.byId[id]?.retainedBy?.mainView ?? 0) > 0);
|
|
1881
|
+
openedRef.current = true;
|
|
1882
|
+
if (typeof current === 'string') mvOpenForSession(current);
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
openedRef.current = true;
|
|
1885
|
+
}
|
|
1886
|
+
}, [
|
|
1887
|
+
list
|
|
1888
|
+
]);
|
|
1889
|
+
React.useEffect(()=>{
|
|
1890
|
+
mvUpdateHydrate();
|
|
1891
|
+
mvUpdateMaybeAutoCheck();
|
|
1892
|
+
}, []);
|
|
1893
|
+
React.useEffect(()=>{
|
|
1894
|
+
const element = rootRef.current;
|
|
1895
|
+
if (element === null) return undefined;
|
|
1896
|
+
const onClick = (event)=>{
|
|
1897
|
+
const target = event.target;
|
|
1898
|
+
if (!(target instanceof Element)) return;
|
|
1899
|
+
const chip = target.closest('button[class*="fileLink"], a[class*="fileLink"]');
|
|
1900
|
+
if (chip === null) return;
|
|
1901
|
+
const pane = chip.closest('[data-pane]');
|
|
1902
|
+
const leafId = pane === null ? null : pane.getAttribute('data-pane-id');
|
|
1903
|
+
const leaf = leafId === null ? null : mvLeafById(mvGetState().layout, leafId);
|
|
1904
|
+
const sessionId = leaf === null ? null : leaf.sessionId;
|
|
1905
|
+
if (sessionId === null) return;
|
|
1906
|
+
event.preventDefault();
|
|
1907
|
+
event.stopPropagation();
|
|
1908
|
+
mvOpenFileInNative(sessionId, (chip.textContent ?? '').trim());
|
|
1909
|
+
};
|
|
1910
|
+
element.addEventListener('click', onClick, true);
|
|
1911
|
+
return ()=>element.removeEventListener('click', onClick, true);
|
|
1912
|
+
}, []);
|
|
1913
|
+
React.useEffect(()=>{
|
|
1914
|
+
const element = rootRef.current;
|
|
1915
|
+
if (element === null || typeof ResizeObserver !== 'function') return undefined;
|
|
1916
|
+
const observer = new ResizeObserver(()=>{
|
|
1917
|
+
setNarrow(element.clientWidth < 900 || element.clientHeight < 520);
|
|
1918
|
+
});
|
|
1919
|
+
observer.observe(element);
|
|
1920
|
+
return ()=>observer.disconnect();
|
|
1921
|
+
}, []);
|
|
1922
|
+
React.useEffect(()=>{
|
|
1923
|
+
const move = (event)=>{
|
|
1924
|
+
const record = dragRef.current;
|
|
1925
|
+
if (!record.active) return;
|
|
1926
|
+
const target = document.elementFromPoint(event.clientX, event.clientY);
|
|
1927
|
+
const pane = target === null ? null : target.closest('[data-pane]');
|
|
1928
|
+
const over = pane === null ? null : pane.getAttribute('data-pane-id');
|
|
1929
|
+
if (over === record.over) return;
|
|
1930
|
+
record.over = over;
|
|
1931
|
+
setDrag({
|
|
1932
|
+
from: record.from,
|
|
1933
|
+
over
|
|
1934
|
+
});
|
|
1935
|
+
};
|
|
1936
|
+
const up = ()=>{
|
|
1937
|
+
const record = dragRef.current;
|
|
1938
|
+
const from = record.from;
|
|
1939
|
+
const over = record.over;
|
|
1940
|
+
dragRef.current = {
|
|
1941
|
+
from: null,
|
|
1942
|
+
over: null,
|
|
1943
|
+
startX: 0,
|
|
1944
|
+
startY: 0,
|
|
1945
|
+
active: false
|
|
1946
|
+
};
|
|
1947
|
+
document.body.style.userSelect = '';
|
|
1948
|
+
setDrag({
|
|
1949
|
+
from: null,
|
|
1950
|
+
over: null
|
|
1951
|
+
});
|
|
1952
|
+
if (record.active && from !== null && over !== null && from !== over) mvSwapLeaves(from, over);
|
|
1953
|
+
};
|
|
1954
|
+
window.addEventListener('pointermove', move);
|
|
1955
|
+
window.addEventListener('pointerup', up);
|
|
1956
|
+
window.addEventListener('pointercancel', up);
|
|
1957
|
+
return ()=>{
|
|
1958
|
+
window.removeEventListener('pointermove', move);
|
|
1959
|
+
window.removeEventListener('pointerup', up);
|
|
1960
|
+
window.removeEventListener('pointercancel', up);
|
|
1961
|
+
};
|
|
1962
|
+
}, []);
|
|
1963
|
+
const surface = {
|
|
1964
|
+
state,
|
|
1965
|
+
statuses,
|
|
1966
|
+
SessionProvider,
|
|
1967
|
+
renderSlot,
|
|
1968
|
+
dragging: drag,
|
|
1969
|
+
onFocus: mvFocus,
|
|
1970
|
+
onResize: mvResizeSplit,
|
|
1971
|
+
onMaximize: mvToggleMaximize,
|
|
1972
|
+
onRemove: mvRemovePane,
|
|
1973
|
+
onBind: mvBind,
|
|
1974
|
+
onOpenMain: (sessionId)=>{
|
|
1975
|
+
try {
|
|
1976
|
+
PTX.uiWorkspace.openSession(sessionId);
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
console.error('dsh-multiview: opening the native view failed', error);
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
onHeadPointerDown: (leafId, event)=>{
|
|
1982
|
+
if (event.button !== 0) return;
|
|
1983
|
+
dragRef.current = {
|
|
1984
|
+
from: leafId,
|
|
1985
|
+
over: null,
|
|
1986
|
+
startX: event.clientX,
|
|
1987
|
+
startY: event.clientY,
|
|
1988
|
+
active: false
|
|
1989
|
+
};
|
|
1990
|
+
const arm = (moveEvent)=>{
|
|
1991
|
+
const record = dragRef.current;
|
|
1992
|
+
if (record.from === null) return;
|
|
1993
|
+
if (record.active) return;
|
|
1994
|
+
const distance = Math.abs(moveEvent.clientX - record.startX) + Math.abs(moveEvent.clientY - record.startY);
|
|
1995
|
+
if (distance < 4) return;
|
|
1996
|
+
record.active = true;
|
|
1997
|
+
document.body.style.userSelect = 'none';
|
|
1998
|
+
setDrag({
|
|
1999
|
+
from: leafId,
|
|
2000
|
+
over: null
|
|
2001
|
+
});
|
|
2002
|
+
};
|
|
2003
|
+
window.addEventListener('pointermove', arm);
|
|
2004
|
+
window.addEventListener('pointerup', ()=>window.removeEventListener('pointermove', arm), {
|
|
2005
|
+
once: true
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
const leaves = mvLeaves(state.layout);
|
|
2010
|
+
const maximizedLeaf = state.maximized === null ? null : mvLeafById(state.layout, state.maximized);
|
|
2011
|
+
const focusedLeaf = leaves.find((leaf)=>leaf.id === state.focused) ?? leaves[0];
|
|
2012
|
+
const visibleLeaf = maximizedLeaf === null ? focusedLeaf : maximizedLeaf;
|
|
2013
|
+
const body = h('div', {
|
|
2014
|
+
className: 'mv-body',
|
|
2015
|
+
'data-mv-body': ''
|
|
2016
|
+
}, renderMvNode(narrow ? visibleLeaf : maximizedLeaf === null ? state.layout : maximizedLeaf, surface));
|
|
2017
|
+
const chips = narrow ? leaves.map((leaf)=>{
|
|
2018
|
+
const title = leaf.sessionId === null ? T('empty') : mvTitle(leaf.sessionId) ?? T('empty');
|
|
2019
|
+
return h('button', {
|
|
2020
|
+
key: 'chip-' + leaf.id,
|
|
2021
|
+
type: 'button',
|
|
2022
|
+
className: 'mv-chip',
|
|
2023
|
+
'data-active': leaf.id === focusedLeaf.id ? 'true' : 'false',
|
|
2024
|
+
title,
|
|
2025
|
+
onClick: ()=>mvFocus(leaf.id)
|
|
2026
|
+
}, title.length > 14 ? title.slice(0, 14) + '…' : title);
|
|
2027
|
+
}) : [];
|
|
2028
|
+
return h('div', {
|
|
2029
|
+
ref: rootRef,
|
|
2030
|
+
className: 'mv-root',
|
|
2031
|
+
'data-multiview': ''
|
|
2032
|
+
}, h('header', {
|
|
2033
|
+
className: 'mv-bar',
|
|
2034
|
+
'data-window-drag': true
|
|
2035
|
+
}, h('span', {
|
|
2036
|
+
className: 'mv-bar-title'
|
|
2037
|
+
}, T('title')), h('div', {
|
|
2038
|
+
className: 'mv-seg',
|
|
2039
|
+
role: 'group',
|
|
2040
|
+
'aria-label': T('shape')
|
|
2041
|
+
}, [
|
|
2042
|
+
2,
|
|
2043
|
+
3,
|
|
2044
|
+
4
|
|
2045
|
+
].map((count)=>h('button', {
|
|
2046
|
+
key: 'shape-' + String(count),
|
|
2047
|
+
type: 'button',
|
|
2048
|
+
className: 'mv-chip',
|
|
2049
|
+
'data-active': leaves.length === count && maximizedLeaf === null ? 'true' : 'false',
|
|
2050
|
+
title: mvFormat(T('shapeN'), {
|
|
2051
|
+
n: count
|
|
2052
|
+
}),
|
|
2053
|
+
'aria-label': mvFormat(T('shapeN'), {
|
|
2054
|
+
n: count
|
|
2055
|
+
}),
|
|
2056
|
+
onClick: ()=>mvSetShape(count)
|
|
2057
|
+
}, String(count)))), state.maximized === null ? null : h('button', {
|
|
2058
|
+
type: 'button',
|
|
2059
|
+
className: 'mv-chip',
|
|
2060
|
+
'data-active': 'true',
|
|
2061
|
+
onClick: mvRestore
|
|
2062
|
+
}, T('restore')), h(MvViewsButton, {
|
|
2063
|
+
views: state.views ?? [],
|
|
2064
|
+
currentKey: mvViewKey(state.layout)
|
|
2065
|
+
}), h(MvUpdateButton, null), h('span', {
|
|
2066
|
+
className: 'mv-bar-spacer'
|
|
2067
|
+
}), h('div', {
|
|
2068
|
+
className: 'mv-strip'
|
|
2069
|
+
}, chips)), h(MvStageBoundary, null, body));
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
const MV_INJECT = [
|
|
2073
|
+
'slots',
|
|
2074
|
+
'sessions',
|
|
2075
|
+
'workspaces',
|
|
2076
|
+
'locale',
|
|
2077
|
+
'layout',
|
|
2078
|
+
'uiWorkspace',
|
|
2079
|
+
'shortcuts'
|
|
2080
|
+
];
|
|
2081
|
+
function mvShortcutDefaults(code) {
|
|
2082
|
+
const binding = {
|
|
2083
|
+
code,
|
|
2084
|
+
modifiers: [
|
|
2085
|
+
'primary',
|
|
2086
|
+
'alt'
|
|
2087
|
+
]
|
|
2088
|
+
};
|
|
2089
|
+
return {
|
|
2090
|
+
'desktop:macos': binding,
|
|
2091
|
+
'desktop:windows': binding,
|
|
2092
|
+
'desktop:linux': binding
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function mvRegisterShortcut(ctx, spec) {
|
|
2096
|
+
const label = ()=>T(spec.labelKey);
|
|
2097
|
+
const resolve = ()=>{
|
|
2098
|
+
const info = PTX.layout.panelInfo.getSnapshot();
|
|
2099
|
+
if (info.activePanelId !== MV_PANEL_ID) return {
|
|
2100
|
+
status: 'pass'
|
|
2101
|
+
};
|
|
2102
|
+
return {
|
|
2103
|
+
status: 'handled',
|
|
2104
|
+
run: ()=>{
|
|
2105
|
+
try {
|
|
2106
|
+
spec.run();
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
console.error('dsh-multiview: shortcut failed', spec.id, error);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
};
|
|
2113
|
+
const base = {
|
|
2114
|
+
id: spec.id,
|
|
2115
|
+
label,
|
|
2116
|
+
aliases: [
|
|
2117
|
+
'multiview',
|
|
2118
|
+
'split'
|
|
2119
|
+
],
|
|
2120
|
+
regions: [
|
|
2121
|
+
'page',
|
|
2122
|
+
'editable'
|
|
2123
|
+
],
|
|
2124
|
+
modals: [],
|
|
2125
|
+
resolve
|
|
2126
|
+
};
|
|
2127
|
+
try {
|
|
2128
|
+
ctx.effect(()=>ctx.shortcuts.register({
|
|
2129
|
+
...base,
|
|
2130
|
+
defaults: mvShortcutDefaults(spec.code)
|
|
2131
|
+
}), 'dsh-multiview: ' + spec.id);
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
console.warn('dsh-multiview: default binding rejected, registering without one:', spec.id, error);
|
|
2134
|
+
ctx.effect(()=>ctx.shortcuts.register({
|
|
2135
|
+
...base,
|
|
2136
|
+
defaults: {}
|
|
2137
|
+
}), 'dsh-multiview: unbound ' + spec.id);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
function mvFocusPane(leafId) {
|
|
2141
|
+
mvFocus(leafId);
|
|
2142
|
+
const leaf = mvLeafById(mvGetState().layout, leafId);
|
|
2143
|
+
if (leaf !== undefined && leaf.sessionId !== null) mvFocusComposer(leaf.sessionId);
|
|
2144
|
+
}
|
|
2145
|
+
function apply(ctx) {
|
|
2146
|
+
PTX = ctx;
|
|
2147
|
+
T = ctx.locale.bind(NS);
|
|
2148
|
+
ctx.effect(()=>ctx.locale.register(NS, {
|
|
2149
|
+
zh: MV_ZH,
|
|
2150
|
+
en: MV_EN
|
|
2151
|
+
}), 'dsh-multiview: dictionaries');
|
|
2152
|
+
ctx.effect(()=>{
|
|
2153
|
+
const tag = document.createElement('style');
|
|
2154
|
+
tag.dataset.plugin = 'dsh-multiview';
|
|
2155
|
+
tag.dataset.pluginCss = 'dsh-multiview/styles';
|
|
2156
|
+
tag.textContent = MV_CSS;
|
|
2157
|
+
document.head.appendChild(tag);
|
|
2158
|
+
return ()=>{
|
|
2159
|
+
tag.remove();
|
|
2160
|
+
};
|
|
2161
|
+
}, 'dsh-multiview: styles');
|
|
2162
|
+
mvHydrate();
|
|
2163
|
+
ctx.slots.inject('main', function*() {
|
|
2164
|
+
yield ctx.slots.register({
|
|
2165
|
+
name: 'main',
|
|
2166
|
+
key: MV_PANEL_ID,
|
|
2167
|
+
locale: NS,
|
|
2168
|
+
children: {
|
|
2169
|
+
[MV_PANE_SLOT]: {
|
|
2170
|
+
kind: 'single',
|
|
2171
|
+
scope: 'session'
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}, MultiviewStage);
|
|
2175
|
+
yield ctx.slots.inject('sidebar.panellist', ()=>ctx.slots.register({
|
|
2176
|
+
name: 'sidebar.panellist',
|
|
2177
|
+
id: MV_PANEL_ID,
|
|
2178
|
+
order: 30,
|
|
2179
|
+
label: ()=>T('panel'),
|
|
2180
|
+
locale: NS
|
|
2181
|
+
}, MultiviewPanelIcon));
|
|
2182
|
+
});
|
|
2183
|
+
ctx.slots.inject(MV_PANE_SLOT, ()=>ctx.slots.register({
|
|
2184
|
+
name: MV_PANE_SLOT,
|
|
2185
|
+
locale: NS
|
|
2186
|
+
}, PaneConversation));
|
|
2187
|
+
mvRegisterShortcut(ctx, {
|
|
2188
|
+
id: 'multiview.pane.focusPrev',
|
|
2189
|
+
labelKey: 'shortcutFocusPrev',
|
|
2190
|
+
code: 'BracketLeft',
|
|
2191
|
+
run: ()=>mvFocusPane(mvNeighbour(mvGetState().layout, mvGetState().focused, -1))
|
|
2192
|
+
});
|
|
2193
|
+
mvRegisterShortcut(ctx, {
|
|
2194
|
+
id: 'multiview.pane.focusNext',
|
|
2195
|
+
labelKey: 'shortcutFocusNext',
|
|
2196
|
+
code: 'BracketRight',
|
|
2197
|
+
run: ()=>mvFocusPane(mvNeighbour(mvGetState().layout, mvGetState().focused, 1))
|
|
2198
|
+
});
|
|
2199
|
+
mvRegisterShortcut(ctx, {
|
|
2200
|
+
id: 'multiview.pane.maximize',
|
|
2201
|
+
labelKey: 'shortcutMaximize',
|
|
2202
|
+
code: 'KeyM',
|
|
2203
|
+
run: ()=>{
|
|
2204
|
+
const state = mvGetState();
|
|
2205
|
+
mvToggleMaximize(state.focused);
|
|
2206
|
+
}
|
|
2207
|
+
});
|
|
2208
|
+
mvRegisterShortcut(ctx, {
|
|
2209
|
+
id: 'multiview.pane.remove',
|
|
2210
|
+
labelKey: 'shortcutRemove',
|
|
2211
|
+
code: 'KeyW',
|
|
2212
|
+
run: ()=>{
|
|
2213
|
+
mvRemovePane(mvGetState().focused);
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
const host = typeof window === 'undefined' ? '' : window.location.hostname;
|
|
2217
|
+
if (host === '127.0.0.1' || host === 'localhost') {
|
|
2218
|
+
window.__mvDebug = {
|
|
2219
|
+
version: '0.1.0',
|
|
2220
|
+
list: ()=>PTX.sessions.list.getSnapshot(),
|
|
2221
|
+
seed: (limit)=>mvSeed(limit),
|
|
2222
|
+
live: ()=>mvConversationLive(),
|
|
2223
|
+
focusComposer: (sessionId)=>mvFocusComposer(sessionId),
|
|
2224
|
+
state: ()=>mvGetState(),
|
|
2225
|
+
shape: (count)=>mvSetShape(count),
|
|
2226
|
+
bind: (leafId, sessionId)=>mvBind(leafId, sessionId),
|
|
2227
|
+
maximize: (leafId)=>mvToggleMaximize(leafId),
|
|
2228
|
+
restore: ()=>mvRestore(),
|
|
2229
|
+
resize: (splitId, ratio)=>mvResizeSplit(splitId, ratio),
|
|
2230
|
+
swap: (a, b)=>mvSwapLeaves(a, b),
|
|
2231
|
+
remove: (leafId)=>mvRemovePane(leafId),
|
|
2232
|
+
reset: ()=>mvReset(),
|
|
2233
|
+
neighbour: (leafId, delta)=>mvNeighbour(mvGetState().layout, leafId, delta),
|
|
2234
|
+
mainKeys: ()=>(PTX.slots.entries('main') || []).map((entry)=>entry.options.key),
|
|
2235
|
+
panelInfo: ()=>PTX.layout.panelInfo.getSnapshot(),
|
|
2236
|
+
paneEntries: ()=>(PTX.slots.entries(MV_PANE_SLOT) || []).length,
|
|
2237
|
+
open: ()=>PTX.layout.selectPanel(MV_PANEL_ID),
|
|
2238
|
+
close: ()=>PTX.layout.selectPanel(null),
|
|
2239
|
+
workspaces: ()=>PTX.workspaces.list.getSnapshot(),
|
|
2240
|
+
openFor: (sessionId)=>mvOpenForSession(sessionId),
|
|
2241
|
+
startView: ()=>mvStartView(),
|
|
2242
|
+
applyView: (viewId)=>mvApplyView(viewId),
|
|
2243
|
+
deleteView: (viewId)=>mvDeleteView(viewId)
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
return { inject: MV_INJECT, apply }
|
|
2249
|
+
},
|
|
2250
|
+
})
|