@vincentbanana/bananacli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/app.js +422 -136
- package/dist/ui/index.html +115 -95
- package/dist/ui/style.css +841 -0
- package/package.json +1 -1
package/dist/ui/app.js
CHANGED
|
@@ -41,6 +41,7 @@ const ICONS = {
|
|
|
41
41
|
maximize: '<rect x="6" y="6" width="12" height="12" rx="1"/>',
|
|
42
42
|
restore: '<rect x="5" y="8" width="9" height="9" rx="1"/><path d="M9 5h9v9"/>',
|
|
43
43
|
diff: '<path d="M6 4v16M18 4v16M3 8h6M15 16h6M3 16h6M15 8h6"/>',
|
|
44
|
+
refresh: '<path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 5v6h-6"/>',
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
const TOOL_ICON = {
|
|
@@ -175,7 +176,11 @@ const state = {
|
|
|
175
176
|
overlay: null,
|
|
176
177
|
ccIndex: 0,
|
|
177
178
|
ccItems: [],
|
|
178
|
-
|
|
179
|
+
tabs: [],
|
|
180
|
+
activeTab: null,
|
|
181
|
+
review: { open: false, mode: 'changes', width: 520 },
|
|
182
|
+
sidebar: { open: true, width: 344 },
|
|
183
|
+
dock: { open: false, height: 280 },
|
|
179
184
|
changes: null,
|
|
180
185
|
changesFile: null,
|
|
181
186
|
};
|
|
@@ -236,7 +241,8 @@ async function switchSession(ref) {
|
|
|
236
241
|
renderSidebar();
|
|
237
242
|
renderThread();
|
|
238
243
|
$('filesTitle').textContent = `项目文件 · ${state.info?.workspace ?? ''}`;
|
|
239
|
-
|
|
244
|
+
setActiveTab(state.info?.session?.id ?? null);
|
|
245
|
+
if (state.review.open && state.review.mode === 'changes') void loadChanges(true);
|
|
240
246
|
closeSidebarIfOverlay();
|
|
241
247
|
} catch (error) {
|
|
242
248
|
toast('切换会话失败:' + (error?.message ?? error));
|
|
@@ -247,10 +253,153 @@ async function switchSession(ref) {
|
|
|
247
253
|
}
|
|
248
254
|
}
|
|
249
255
|
|
|
256
|
+
// ---------------- 会话标签条 ----------------
|
|
257
|
+
function tabTitleFor(id) {
|
|
258
|
+
if (state.info?.session?.id === id && state.info.session.title) return state.info.session.title;
|
|
259
|
+
return state.sessions.find((item) => item.id === id)?.title || '新对话';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function saveTabs() {
|
|
263
|
+
try {
|
|
264
|
+
localStorage.setItem(
|
|
265
|
+
SHELL_STORAGE.tabState,
|
|
266
|
+
JSON.stringify({ all: state.tabs, active: state.activeTab }),
|
|
267
|
+
);
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function upsertTab(id, title) {
|
|
272
|
+
if (!id) return;
|
|
273
|
+
const existing = state.tabs.find((tab) => tab.id === id);
|
|
274
|
+
if (existing) {
|
|
275
|
+
if (title) existing.title = title;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
state.tabs.push({ id, title: title || tabTitleFor(id) });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function setActiveTab(id) {
|
|
282
|
+
state.activeTab = id;
|
|
283
|
+
if (id) upsertTab(id);
|
|
284
|
+
saveTabs();
|
|
285
|
+
renderTabs();
|
|
286
|
+
renderSidebar();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function renderTabs() {
|
|
290
|
+
const strip = $('tabStrip');
|
|
291
|
+
if (!strip) return;
|
|
292
|
+
strip.innerHTML = '';
|
|
293
|
+
for (const tab of state.tabs) {
|
|
294
|
+
const node = document.createElement('button');
|
|
295
|
+
node.type = 'button';
|
|
296
|
+
node.className = `tab${tab.id === state.activeTab ? ' is-active' : ''}`;
|
|
297
|
+
node.draggable = true;
|
|
298
|
+
node.dataset.id = tab.id;
|
|
299
|
+
const title = tabTitleFor(tab.id) || tab.title || '新对话';
|
|
300
|
+
node.innerHTML =
|
|
301
|
+
`<span class="tab-avatar">${escapeHtml(title.slice(0, 1))}</span>` +
|
|
302
|
+
`<span class="tab-title">${escapeHtml(title)}</span>` +
|
|
303
|
+
`<span class="tab-close" title="关闭标签"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></span>`;
|
|
304
|
+
node.addEventListener('pointerdown', (event) => {
|
|
305
|
+
if (event.button !== 0 || event.target.closest('.tab-close')) return;
|
|
306
|
+
if (tab.id !== state.activeTab) {
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
switchSession(tab.id);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
node.addEventListener('click', (event) => {
|
|
312
|
+
if (event.target.closest('.tab-close')) {
|
|
313
|
+
event.stopPropagation();
|
|
314
|
+
closeTab(tab.id);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (event.detail === 0) switchSession(tab.id);
|
|
318
|
+
});
|
|
319
|
+
node.addEventListener('auxclick', (event) => {
|
|
320
|
+
if (event.button === 1) closeTab(tab.id);
|
|
321
|
+
});
|
|
322
|
+
node.addEventListener('dragstart', (event) => {
|
|
323
|
+
event.dataTransfer.setData('text/plain', tab.id);
|
|
324
|
+
node.classList.add('is-dragging');
|
|
325
|
+
});
|
|
326
|
+
node.addEventListener('dragend', () => node.classList.remove('is-dragging'));
|
|
327
|
+
node.addEventListener('dragover', (event) => event.preventDefault());
|
|
328
|
+
node.addEventListener('drop', (event) => {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
const source = event.dataTransfer.getData('text/plain');
|
|
331
|
+
const from = state.tabs.findIndex((item) => item.id === source);
|
|
332
|
+
const to = state.tabs.findIndex((item) => item.id === tab.id);
|
|
333
|
+
if (from < 0 || to < 0 || from === to) return;
|
|
334
|
+
const [picked] = state.tabs.splice(from, 1);
|
|
335
|
+
state.tabs.splice(to, 0, picked);
|
|
336
|
+
saveTabs();
|
|
337
|
+
renderTabs();
|
|
338
|
+
});
|
|
339
|
+
strip.appendChild(node);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function closeTab(id) {
|
|
344
|
+
const index = state.tabs.findIndex((tab) => tab.id === id);
|
|
345
|
+
if (index < 0) return;
|
|
346
|
+
state.tabs.splice(index, 1);
|
|
347
|
+
saveTabs();
|
|
348
|
+
if (state.activeTab !== id) {
|
|
349
|
+
renderTabs();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (state.tabs.length) {
|
|
353
|
+
const next = state.tabs[Math.max(0, index - 1)] ?? state.tabs[0];
|
|
354
|
+
switchSession(next.id);
|
|
355
|
+
} else {
|
|
356
|
+
state.activeTab = null;
|
|
357
|
+
renderTabs();
|
|
358
|
+
$('newChat').click();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function initTabs() {
|
|
363
|
+
let saved = null;
|
|
364
|
+
try {
|
|
365
|
+
saved = JSON.parse(localStorage.getItem(SHELL_STORAGE.tabState) || 'null');
|
|
366
|
+
} catch {}
|
|
367
|
+
if (saved && Array.isArray(saved.all)) {
|
|
368
|
+
const known = new Set(state.sessions.map((item) => item.id));
|
|
369
|
+
if (state.info?.session?.id) known.add(state.info.session.id);
|
|
370
|
+
state.tabs = saved.all.filter((tab) => tab && typeof tab.id === 'string' && known.has(tab.id));
|
|
371
|
+
state.activeTab = saved.active ?? null;
|
|
372
|
+
}
|
|
373
|
+
const current = state.info?.session?.id;
|
|
374
|
+
if (current) upsertTab(current, state.info.session.title);
|
|
375
|
+
if (!state.activeTab || !state.tabs.some((tab) => tab.id === state.activeTab)) {
|
|
376
|
+
state.activeTab = current ?? state.tabs[0]?.id ?? null;
|
|
377
|
+
}
|
|
378
|
+
renderTabs();
|
|
379
|
+
}
|
|
380
|
+
|
|
250
381
|
function renderSidebar() {
|
|
251
382
|
const list = $('sessionList');
|
|
252
383
|
list.innerHTML = '';
|
|
384
|
+
let group = '';
|
|
385
|
+
const dayOf = (iso) => {
|
|
386
|
+
const date = new Date(iso);
|
|
387
|
+
const now = new Date();
|
|
388
|
+
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
389
|
+
const value = date.getTime();
|
|
390
|
+
if (value >= start) return '今天';
|
|
391
|
+
if (value >= start - 86_400_000) return '昨天';
|
|
392
|
+
return '更早';
|
|
393
|
+
};
|
|
253
394
|
state.sessions.forEach((item, index) => {
|
|
395
|
+
const label = dayOf(item.updatedAt);
|
|
396
|
+
if (label !== group) {
|
|
397
|
+
group = label;
|
|
398
|
+
const head = document.createElement('div');
|
|
399
|
+
head.className = 'sb-group';
|
|
400
|
+
head.textContent = label;
|
|
401
|
+
list.appendChild(head);
|
|
402
|
+
}
|
|
254
403
|
const active = item.id === state.info?.session?.id ? ' is-active' : '';
|
|
255
404
|
const node = document.createElement('div');
|
|
256
405
|
node.className = `session-item${active}`;
|
|
@@ -270,7 +419,11 @@ function renderSidebar() {
|
|
|
270
419
|
|
|
271
420
|
node.querySelector('[data-del]').addEventListener('click', (event) => {
|
|
272
421
|
event.stopPropagation();
|
|
273
|
-
post('session', { action: 'delete', ref: item.id }).then(() =>
|
|
422
|
+
post('session', { action: 'delete', ref: item.id }).then(() => {
|
|
423
|
+
state.tabs = state.tabs.filter((tab) => tab.id !== item.id);
|
|
424
|
+
saveTabs();
|
|
425
|
+
refresh(true);
|
|
426
|
+
});
|
|
274
427
|
});
|
|
275
428
|
|
|
276
429
|
node.querySelector('[data-menu]').addEventListener('click', (event) => {
|
|
@@ -425,36 +578,42 @@ function renderMessage(message) {
|
|
|
425
578
|
function renderEmpty() {
|
|
426
579
|
const wrap = document.createElement('div');
|
|
427
580
|
wrap.className = 'empty';
|
|
428
|
-
//
|
|
429
|
-
const provider = state.info?.provider;
|
|
430
|
-
const current = state.info?.providers?.find((item) => item.current);
|
|
431
|
-
const hasKey = Boolean(current?.keyReady);
|
|
432
|
-
const banner = hasKey
|
|
433
|
-
? ''
|
|
434
|
-
: `<div class="empty-warn">
|
|
435
|
-
<strong>还差一步:填上 API Key</strong>
|
|
436
|
-
<span>没有 key 就没法对话。点下面的按钮,填进去立刻就能用(key 只存在你自己的电脑上)。</span>
|
|
437
|
-
<button class="btn primary" id="emptySetup">现在去填</button>
|
|
438
|
-
</div>`;
|
|
581
|
+
// 新会话:只保留 wordmark,操作提示放到 composer 下方,和 opencode 的首页一致
|
|
439
582
|
wrap.innerHTML =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
'<div class="suggest">' +
|
|
444
|
-
['这个仓库是做什么的', '跑一下测试并修复失败', '给我一个重构方案,先别改代码', '把 README 翻译成英文']
|
|
445
|
-
.map((text) => `<button>${escapeHtml(text)}</button>`)
|
|
446
|
-
.join('') +
|
|
447
|
-
'</div>' +
|
|
448
|
-
`<div class="empty-tips">
|
|
449
|
-
<span><b>/</b> 调命令(/help 看全部)</span>
|
|
450
|
-
<span><b>@</b> 引用项目里的文件</span>
|
|
451
|
-
<span><b>${escapeHtml(provider?.name ?? '')}</b> 当前模型</span>
|
|
583
|
+
`<div class="home-wordmark" aria-hidden="true">
|
|
584
|
+
<span class="wm-text">Banana</span>
|
|
585
|
+
<svg class="wm-leaf" viewBox="0 0 72 72"><path d="M14 42c2-16 14-27 34-28 0 18-8 31-24 35"/><path d="M14 58c8-15 18-24 32-31"/><path d="M44 14c-3 7-4 13-3 20"/></svg>
|
|
452
586
|
</div>`;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
587
|
+
|
|
588
|
+
const tips = $('homeTips');
|
|
589
|
+
if (tips) {
|
|
590
|
+
const provider = state.info?.provider;
|
|
591
|
+
const current = state.info?.providers?.find((item) => item.current);
|
|
592
|
+
const hasKey = Boolean(current?.keyReady);
|
|
593
|
+
tips.innerHTML =
|
|
594
|
+
(hasKey
|
|
595
|
+
? ''
|
|
596
|
+
: `<div class="empty-warn">
|
|
597
|
+
<strong>还差一步:填上 API Key</strong>
|
|
598
|
+
<span>没有 key 就没法对话。点下面的按钮,填进去立刻就能用(key 只存在你自己的电脑上)。</span>
|
|
599
|
+
<button class="btn primary" id="emptySetup">现在去填</button>
|
|
600
|
+
</div>`) +
|
|
601
|
+
'<div class="suggest">' +
|
|
602
|
+
['这个仓库是做什么的', '跑一下测试并修复失败', '给我一个重构方案,先别改代码', '把 README 翻译成英文']
|
|
603
|
+
.map((text) => `<button>${escapeHtml(text)}</button>`)
|
|
604
|
+
.join('') +
|
|
605
|
+
'</div>' +
|
|
606
|
+
`<div class="empty-tips">
|
|
607
|
+
<span><b>/</b> 调命令(/help 看全部)</span>
|
|
608
|
+
<span><b>@</b> 引用项目里的文件</span>
|
|
609
|
+
<span><b>${escapeHtml(provider?.name ?? '')}</b> 当前模型</span>
|
|
610
|
+
</div>`;
|
|
611
|
+
tips.querySelectorAll('.suggest button').forEach((btn) => {
|
|
612
|
+
btn.addEventListener('click', () => { $('input').value = btn.textContent; $('input').focus(); grow(); });
|
|
613
|
+
});
|
|
614
|
+
const setup = tips.querySelector('#emptySetup');
|
|
615
|
+
if (setup) setup.addEventListener('click', () => openSettings());
|
|
616
|
+
}
|
|
458
617
|
return wrap;
|
|
459
618
|
}
|
|
460
619
|
|
|
@@ -472,6 +631,7 @@ function renderThread() {
|
|
|
472
631
|
if (node) THREAD.appendChild(node);
|
|
473
632
|
}
|
|
474
633
|
renderTurnMeta();
|
|
634
|
+
updateHomeMode();
|
|
475
635
|
// 切换会话时瞬间定位到底部;平滑滚动只用于新消息
|
|
476
636
|
scrollToEnd(false);
|
|
477
637
|
}
|
|
@@ -542,8 +702,13 @@ async function refresh(path) {
|
|
|
542
702
|
state.info = info;
|
|
543
703
|
const { sessions } = await get('sessions');
|
|
544
704
|
state.sessions = sessions;
|
|
705
|
+
if (state.info?.session?.id && state.tabs.length) {
|
|
706
|
+
upsertTab(state.info.session.id, state.info.session.title);
|
|
707
|
+
state.activeTab = state.info.session.id;
|
|
708
|
+
}
|
|
545
709
|
renderHeader();
|
|
546
710
|
renderSidebar();
|
|
711
|
+
renderTabs();
|
|
547
712
|
if (path) renderThread();
|
|
548
713
|
}
|
|
549
714
|
|
|
@@ -572,7 +737,6 @@ function syncOverlayState() {
|
|
|
572
737
|
!$('commandCenter').hidden ||
|
|
573
738
|
!$('codeViewer').hidden ||
|
|
574
739
|
!$('settingsSheet').hidden ||
|
|
575
|
-
!$('workbench').hidden ||
|
|
576
740
|
!$('approvalModal').hidden;
|
|
577
741
|
document.body.classList.toggle('has-overlay', open);
|
|
578
742
|
}
|
|
@@ -583,7 +747,7 @@ function closeTopOverlay() {
|
|
|
583
747
|
if (!$('codeViewer').hidden) return closeViewer();
|
|
584
748
|
if (!$('settingsSheet').hidden) return closeSettings();
|
|
585
749
|
if (!$('approvalModal').hidden) return closeApproval(false);
|
|
586
|
-
if (
|
|
750
|
+
if (state.review.open) return closeReview();
|
|
587
751
|
return false;
|
|
588
752
|
}
|
|
589
753
|
|
|
@@ -742,6 +906,7 @@ async function send() {
|
|
|
742
906
|
userBody.textContent = text;
|
|
743
907
|
userMsg.appendChild(userBody);
|
|
744
908
|
THREAD.appendChild(userMsg);
|
|
909
|
+
updateHomeMode();
|
|
745
910
|
|
|
746
911
|
const botMsg = document.createElement('article');
|
|
747
912
|
botMsg.className = 'msg assistant enter';
|
|
@@ -886,6 +1051,11 @@ async function send() {
|
|
|
886
1051
|
row.addEventListener('click', () => toggleToolOutput(row, event.detail, !event.ok));
|
|
887
1052
|
}
|
|
888
1053
|
}
|
|
1054
|
+
} else if (event.type === 'status') {
|
|
1055
|
+
if (event.message) {
|
|
1056
|
+
$('composerHint').textContent = event.message;
|
|
1057
|
+
$('threadMeta').textContent = event.message;
|
|
1058
|
+
}
|
|
889
1059
|
} else if (event.type === 'approval') {
|
|
890
1060
|
const allowed = await askApproval(event);
|
|
891
1061
|
await post('approve', { id: event.id, allowed });
|
|
@@ -918,8 +1088,9 @@ async function send() {
|
|
|
918
1088
|
const { sessions } = await get('sessions');
|
|
919
1089
|
state.sessions = sessions;
|
|
920
1090
|
renderSidebar();
|
|
1091
|
+
renderTabs();
|
|
921
1092
|
renderHeader();
|
|
922
|
-
if (state.
|
|
1093
|
+
if (state.review.open && state.review.mode === 'changes') void loadChanges(true);
|
|
923
1094
|
}
|
|
924
1095
|
}
|
|
925
1096
|
}
|
|
@@ -943,7 +1114,8 @@ async function send() {
|
|
|
943
1114
|
finishWork();
|
|
944
1115
|
setBusy(false);
|
|
945
1116
|
scrollToEnd();
|
|
946
|
-
|
|
1117
|
+
renderHeader();
|
|
1118
|
+
if (state.review.open && state.review.mode === 'changes') void loadChanges(true);
|
|
947
1119
|
}
|
|
948
1120
|
}
|
|
949
1121
|
|
|
@@ -1117,6 +1289,7 @@ async function runCommand(text) {
|
|
|
1117
1289
|
userBody.textContent = text;
|
|
1118
1290
|
userMsg.appendChild(userBody);
|
|
1119
1291
|
THREAD.appendChild(userMsg);
|
|
1292
|
+
updateHomeMode();
|
|
1120
1293
|
|
|
1121
1294
|
const botMsg = document.createElement('article');
|
|
1122
1295
|
botMsg.className = 'msg assistant enter';
|
|
@@ -1509,10 +1682,10 @@ function renderTerminalSettings(body) {
|
|
|
1509
1682
|
});
|
|
1510
1683
|
card.appendChild(settingsRow('默认 Shell', '新建终端标签时使用。', shellSelect));
|
|
1511
1684
|
|
|
1512
|
-
const isTermOpen = state.
|
|
1685
|
+
const isTermOpen = state.dock.open;
|
|
1513
1686
|
const openBtn = settingsButton(isTermOpen ? '收起终端' : '打开终端', () => {
|
|
1514
1687
|
closeSettings();
|
|
1515
|
-
if (isTermOpen)
|
|
1688
|
+
if (isTermOpen) termClose();
|
|
1516
1689
|
else termOpen();
|
|
1517
1690
|
}, true);
|
|
1518
1691
|
card.appendChild(settingsRow('终端面板', '在右侧工作台打开,不会挡住输入框。快捷键 Ctrl+`。', openBtn));
|
|
@@ -1769,113 +1942,144 @@ function renderAboutSettings(body) {
|
|
|
1769
1942
|
body.appendChild(section);
|
|
1770
1943
|
}
|
|
1771
1944
|
|
|
1772
|
-
// ----------------
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1945
|
+
// ---------------- 外壳:侧栏 / Review / 终端 dock ----------------
|
|
1946
|
+
const SHELL_STORAGE = {
|
|
1947
|
+
sidebarWidth: 'banana.sidebar.width',
|
|
1948
|
+
sidebarOpen: 'banana.sidebar.open',
|
|
1949
|
+
reviewWidth: 'banana.review.width',
|
|
1950
|
+
reviewMode: 'banana.review.mode',
|
|
1951
|
+
tabState: 'banana.tabs.v1',
|
|
1952
|
+
dockHeight: 'banana.dock.height',
|
|
1953
|
+
dockOpen: 'banana.dock.open',
|
|
1954
|
+
};
|
|
1776
1955
|
|
|
1777
|
-
function
|
|
1778
|
-
const max = Math.max(
|
|
1779
|
-
state.
|
|
1780
|
-
|
|
1956
|
+
function applySidebarWidth(width) {
|
|
1957
|
+
const max = Math.max(240, Math.min(430, window.innerWidth * 0.42));
|
|
1958
|
+
state.sidebar.width = Math.max(240, Math.min(Number(width) || 344, max));
|
|
1959
|
+
document.documentElement.style.setProperty('--sidebar', `${Math.round(state.sidebar.width)}px`);
|
|
1781
1960
|
}
|
|
1782
1961
|
|
|
1783
|
-
function
|
|
1784
|
-
|
|
1962
|
+
function setSidebar(open) {
|
|
1963
|
+
state.sidebar.open = Boolean(open);
|
|
1964
|
+
$('sidebar').classList.toggle('open', state.sidebar.open && window.matchMedia('(max-width: 900px)').matches);
|
|
1965
|
+
$('sidebar').hidden = !state.sidebar.open && !window.matchMedia('(max-width: 900px)').matches;
|
|
1966
|
+
if (window.matchMedia('(max-width: 900px)').matches) $('sidebar').hidden = false;
|
|
1967
|
+
try {
|
|
1968
|
+
localStorage.setItem(SHELL_STORAGE.sidebarOpen, state.sidebar.open ? '1' : '0');
|
|
1969
|
+
} catch {}
|
|
1785
1970
|
}
|
|
1786
1971
|
|
|
1787
|
-
function
|
|
1788
|
-
|
|
1789
|
-
if (tab) state.workbench.tab = tab;
|
|
1790
|
-
wb.classList.remove('is-closing');
|
|
1791
|
-
wb.hidden = false;
|
|
1792
|
-
state.workbench.open = true;
|
|
1793
|
-
wbApplyWidth(state.workbench.width);
|
|
1794
|
-
// 强制读一次布局,保证从 0 宽开始过渡(不依赖可能被后台节流的 rAF)
|
|
1795
|
-
void wb.offsetWidth;
|
|
1796
|
-
wb.classList.add('open');
|
|
1797
|
-
setWorkbenchTab(state.workbench.tab);
|
|
1798
|
-
$('wbScrim').hidden = !wbIsNarrow();
|
|
1799
|
-
syncOverlayState();
|
|
1972
|
+
function toggleSidebar() {
|
|
1973
|
+
setSidebar(!state.sidebar.open || (window.matchMedia('(max-width: 900px)').matches && !$('sidebar').classList.contains('open')));
|
|
1800
1974
|
}
|
|
1801
1975
|
|
|
1802
|
-
function
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
state.
|
|
1806
|
-
wb.classList.remove('open');
|
|
1807
|
-
wb.classList.add('is-closing');
|
|
1808
|
-
$('wbScrim').hidden = true;
|
|
1809
|
-
setTimeout(() => {
|
|
1810
|
-
if (state.workbench.open) return;
|
|
1811
|
-
wb.hidden = true;
|
|
1812
|
-
wb.classList.remove('is-closing');
|
|
1813
|
-
syncOverlayState();
|
|
1814
|
-
$('input')?.focus();
|
|
1815
|
-
}, 280);
|
|
1976
|
+
function applyReviewWidth(width) {
|
|
1977
|
+
const max = Math.max(480, Math.min(800, window.innerWidth * 0.7));
|
|
1978
|
+
state.review.width = Math.max(480, Math.min(Number(width) || 520, max));
|
|
1979
|
+
$('reviewPanel').style.setProperty('--review-width', `${Math.round(state.review.width)}px`);
|
|
1816
1980
|
}
|
|
1817
1981
|
|
|
1818
|
-
function
|
|
1819
|
-
if (!['
|
|
1820
|
-
state.
|
|
1982
|
+
function setReviewMode(mode) {
|
|
1983
|
+
if (!['changes', 'files'].includes(mode)) mode = 'changes';
|
|
1984
|
+
state.review.mode = mode;
|
|
1821
1985
|
try {
|
|
1822
|
-
localStorage.setItem(
|
|
1986
|
+
localStorage.setItem(SHELL_STORAGE.reviewMode, mode);
|
|
1823
1987
|
} catch {}
|
|
1824
|
-
$('
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if (tab === 'terminal') {
|
|
1833
|
-
termEnsure();
|
|
1834
|
-
requestAnimationFrame(() => termFitActive());
|
|
1835
|
-
} else if (tab === 'files') {
|
|
1988
|
+
$('reviewTabChanges').classList.toggle('is-on', mode === 'changes');
|
|
1989
|
+
$('reviewTabFiles').classList.toggle('is-on', mode === 'files');
|
|
1990
|
+
$('changesPanel').classList.toggle('is-on', mode === 'changes');
|
|
1991
|
+
$('filesPanel').classList.toggle('is-on', mode === 'files');
|
|
1992
|
+
if (mode === 'changes') void loadChanges();
|
|
1993
|
+
else {
|
|
1994
|
+
applyFilesLayout();
|
|
1995
|
+
$('filesTitle').textContent = `工作区文件 · ${state.info?.workspace ?? ''}`;
|
|
1836
1996
|
if (!$('filesTree').childElementCount) void loadTree('.');
|
|
1837
|
-
} else if (tab === 'changes') {
|
|
1838
|
-
void loadChanges(true);
|
|
1839
1997
|
}
|
|
1840
1998
|
}
|
|
1841
1999
|
|
|
1842
|
-
function
|
|
2000
|
+
function openReview(mode = 'changes') {
|
|
2001
|
+
state.review.open = true;
|
|
2002
|
+
const panel = $('reviewPanel');
|
|
2003
|
+
panel.classList.remove('is-closing');
|
|
2004
|
+
panel.hidden = false;
|
|
2005
|
+
applyReviewWidth(state.review.width);
|
|
2006
|
+
setReviewMode(mode);
|
|
2007
|
+
syncOverlayState();
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function closeReview() {
|
|
2011
|
+
const panel = $('reviewPanel');
|
|
2012
|
+
if (!state.review.open) return;
|
|
2013
|
+
state.review.open = false;
|
|
2014
|
+
panel.classList.add('is-closing');
|
|
2015
|
+
setTimeout(() => {
|
|
2016
|
+
if (state.review.open) return;
|
|
2017
|
+
panel.hidden = true;
|
|
2018
|
+
panel.classList.remove('is-closing');
|
|
2019
|
+
syncOverlayState();
|
|
2020
|
+
$('input')?.focus();
|
|
2021
|
+
termFitActive();
|
|
2022
|
+
}, 200);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function initShell() {
|
|
1843
2026
|
try {
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2027
|
+
const sw = Number(localStorage.getItem(SHELL_STORAGE.sidebarWidth));
|
|
2028
|
+
applySidebarWidth(sw || 344);
|
|
2029
|
+
const rw = Number(localStorage.getItem(SHELL_STORAGE.reviewWidth));
|
|
2030
|
+
applyReviewWidth(rw || 520);
|
|
2031
|
+
const rm = localStorage.getItem(SHELL_STORAGE.reviewMode);
|
|
2032
|
+
if (rm) state.review.mode = rm;
|
|
2033
|
+
const dh = Number(localStorage.getItem(SHELL_STORAGE.dockHeight));
|
|
2034
|
+
if (dh) state.dock.height = Math.max(120, Math.min(dh, window.innerHeight * 0.6));
|
|
2035
|
+
const openSidebar = localStorage.getItem(SHELL_STORAGE.sidebarOpen);
|
|
2036
|
+
state.sidebar.open = openSidebar === null ? window.innerWidth > 900 : openSidebar !== '0';
|
|
2037
|
+
} catch {
|
|
2038
|
+
applySidebarWidth(344);
|
|
2039
|
+
applyReviewWidth(520);
|
|
2040
|
+
}
|
|
2041
|
+
setSidebar(state.sidebar.open);
|
|
2042
|
+
$('reviewTabChanges').addEventListener('click', () => setReviewMode('changes'));
|
|
2043
|
+
$('reviewTabFiles').addEventListener('click', () => setReviewMode('files'));
|
|
2044
|
+
$('reviewClose').addEventListener('click', closeReview);
|
|
2045
|
+
$('reviewRefresh').addEventListener('click', () => {
|
|
2046
|
+
state.changes = null;
|
|
2047
|
+
if (state.review.mode === 'changes') void loadChanges(true);
|
|
2048
|
+
else $('filesTree').innerHTML = '', void loadTree('.');
|
|
2049
|
+
});
|
|
2050
|
+
$('sbResizer').addEventListener('pointerdown', (event) => {
|
|
2051
|
+
event.preventDefault();
|
|
2052
|
+
const move = (moveEvent) => applySidebarWidth(moveEvent.clientX - 8);
|
|
2053
|
+
const up = () => {
|
|
2054
|
+
window.removeEventListener('pointermove', move);
|
|
2055
|
+
window.removeEventListener('pointerup', up);
|
|
2056
|
+
try {
|
|
2057
|
+
localStorage.setItem(SHELL_STORAGE.sidebarWidth, String(Math.round(state.sidebar.width)));
|
|
2058
|
+
} catch {}
|
|
2059
|
+
};
|
|
2060
|
+
window.addEventListener('pointermove', move);
|
|
2061
|
+
window.addEventListener('pointerup', up);
|
|
1852
2062
|
});
|
|
1853
|
-
$('
|
|
1854
|
-
$('wbScrim').addEventListener('click', closeWorkbench);
|
|
1855
|
-
$('changesRefresh').addEventListener('click', () => loadChanges(true));
|
|
1856
|
-
$('wbResizer').addEventListener('pointerdown', (event) => {
|
|
2063
|
+
$('reviewResizer').addEventListener('pointerdown', (event) => {
|
|
1857
2064
|
event.preventDefault();
|
|
1858
|
-
$('workbench').classList.add('is-resizing');
|
|
1859
2065
|
const move = (moveEvent) => {
|
|
1860
|
-
|
|
1861
|
-
|
|
2066
|
+
applyReviewWidth(window.innerWidth - 8 - moveEvent.clientX);
|
|
2067
|
+
termFitActive();
|
|
1862
2068
|
};
|
|
1863
2069
|
const up = () => {
|
|
1864
2070
|
window.removeEventListener('pointermove', move);
|
|
1865
2071
|
window.removeEventListener('pointerup', up);
|
|
1866
|
-
$('workbench').classList.remove('is-resizing');
|
|
1867
2072
|
try {
|
|
1868
|
-
localStorage.setItem(
|
|
2073
|
+
localStorage.setItem(SHELL_STORAGE.reviewWidth, String(Math.round(state.review.width)));
|
|
1869
2074
|
} catch {}
|
|
1870
|
-
|
|
2075
|
+
termFitActive();
|
|
1871
2076
|
};
|
|
1872
2077
|
window.addEventListener('pointermove', move);
|
|
1873
2078
|
window.addEventListener('pointerup', up);
|
|
1874
2079
|
});
|
|
1875
2080
|
window.addEventListener('resize', () => {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
if (state.workbench.tab === 'terminal') termFitActive();
|
|
2081
|
+
applyReviewWidth(state.review.width);
|
|
2082
|
+
termFitActive();
|
|
1879
2083
|
});
|
|
1880
2084
|
}
|
|
1881
2085
|
|
|
@@ -1899,14 +2103,18 @@ async function loadChanges(force = false) {
|
|
|
1899
2103
|
function renderChanges() {
|
|
1900
2104
|
const list = $('changesList');
|
|
1901
2105
|
const detail = $('changesDetail');
|
|
2106
|
+
const stats = $('reviewStats');
|
|
1902
2107
|
list.innerHTML = '';
|
|
1903
2108
|
const turns = state.changes?.turns ?? [];
|
|
1904
2109
|
if (!turns.length) {
|
|
1905
|
-
|
|
2110
|
+
stats.textContent = '';
|
|
2111
|
+
list.innerHTML = '<div class="changes-empty"><strong>还没有改动</strong><span>让 Banana 改一次文件后,这里会出现增删行。</span></div>';
|
|
1906
2112
|
detail.innerHTML = '<div class="hint">没有可显示的改动。</div>';
|
|
1907
2113
|
return;
|
|
1908
2114
|
}
|
|
1909
2115
|
let total = 0;
|
|
2116
|
+
let additions = 0;
|
|
2117
|
+
let deletions = 0;
|
|
1910
2118
|
for (const turn of turns) {
|
|
1911
2119
|
const group = document.createElement('div');
|
|
1912
2120
|
group.className = 'changes-group';
|
|
@@ -1916,9 +2124,12 @@ function renderChanges() {
|
|
|
1916
2124
|
group.appendChild(head);
|
|
1917
2125
|
for (const file of turn.files) {
|
|
1918
2126
|
total += 1;
|
|
2127
|
+
additions += file.additions;
|
|
2128
|
+
deletions += file.deletions;
|
|
1919
2129
|
const button = document.createElement('button');
|
|
1920
2130
|
button.className = 'change-file';
|
|
1921
2131
|
button.innerHTML =
|
|
2132
|
+
`<span class="change-status ${file.missing ? 'deleted' : file.existed ? 'modified' : 'added'}">${file.missing ? 'D' : file.existed ? 'M' : 'A'}</span>` +
|
|
1922
2133
|
`<span class="cf-name">${escapeHtml(file.path)}</span>` +
|
|
1923
2134
|
`<span class="cf-count"><i class="plus">+${file.additions}</i><i class="minus">-${file.deletions}</i></span>`;
|
|
1924
2135
|
button.addEventListener('click', () => {
|
|
@@ -1930,7 +2141,7 @@ function renderChanges() {
|
|
|
1930
2141
|
}
|
|
1931
2142
|
list.appendChild(group);
|
|
1932
2143
|
}
|
|
1933
|
-
|
|
2144
|
+
stats.innerHTML = `<i class="plus">+${additions}</i> <i class="minus">-${deletions}</i> · ${total} 个文件`;
|
|
1934
2145
|
// 默认打开第一个有内容的文件
|
|
1935
2146
|
const firstFile = turns.find((turn) => turn.files.length)?.files[0];
|
|
1936
2147
|
if (firstFile) {
|
|
@@ -2043,21 +2254,21 @@ function applyFilesLayout() {
|
|
|
2043
2254
|
|
|
2044
2255
|
function openFiles() {
|
|
2045
2256
|
applyFilesLayout();
|
|
2046
|
-
$('filesTitle').textContent =
|
|
2047
|
-
|
|
2257
|
+
$('filesTitle').textContent = `工作区文件 · ${state.info?.workspace ?? ''}`;
|
|
2258
|
+
openReview('files');
|
|
2048
2259
|
}
|
|
2049
2260
|
|
|
2050
2261
|
function openChanges() {
|
|
2051
|
-
|
|
2262
|
+
openReview('changes');
|
|
2052
2263
|
void loadChanges(true);
|
|
2053
2264
|
}
|
|
2054
2265
|
|
|
2055
2266
|
function closeFiles() {
|
|
2056
|
-
|
|
2267
|
+
closeReview();
|
|
2057
2268
|
}
|
|
2058
2269
|
|
|
2059
2270
|
function toggleFiles() {
|
|
2060
|
-
if (
|
|
2271
|
+
if (state.review.open && state.review.mode === 'files') closeReview();
|
|
2061
2272
|
else openFiles();
|
|
2062
2273
|
}
|
|
2063
2274
|
|
|
@@ -2193,7 +2404,7 @@ function termTheme() {
|
|
|
2193
2404
|
foreground: dark ? '#d9d9d4' : '#26262b',
|
|
2194
2405
|
cursor: dark ? '#e8e8e2' : '#16161a',
|
|
2195
2406
|
cursorAccent: dark ? '#101013' : '#fbfbfa',
|
|
2196
|
-
selectionBackground: dark ? 'rgba(
|
|
2407
|
+
selectionBackground: dark ? 'rgba(220, 222, 141, 0.3)' : 'rgba(138, 109, 18, 0.18)',
|
|
2197
2408
|
black: dark ? '#2a2a30' : '#3a3a40',
|
|
2198
2409
|
red: dark ? '#d1786e' : '#b4453a',
|
|
2199
2410
|
green: dark ? '#86b98d' : '#2f7d50',
|
|
@@ -2218,7 +2429,7 @@ function termActiveTab() {
|
|
|
2218
2429
|
}
|
|
2219
2430
|
|
|
2220
2431
|
function termFitActive() {
|
|
2221
|
-
if ($('
|
|
2432
|
+
if ($('terminalPanel').hidden) return;
|
|
2222
2433
|
const tab = termActiveTab();
|
|
2223
2434
|
if (!tab) return;
|
|
2224
2435
|
try {
|
|
@@ -2229,6 +2440,18 @@ function termFitActive() {
|
|
|
2229
2440
|
}
|
|
2230
2441
|
}
|
|
2231
2442
|
|
|
2443
|
+
function applyDockHeight(height, persist = true) {
|
|
2444
|
+
const max = Math.max(160, window.innerHeight * 0.6);
|
|
2445
|
+
state.dock.height = Math.max(120, Math.min(Number(height) || 280, max));
|
|
2446
|
+
$('terminalPanel').style.setProperty('--term-height', `${Math.round(state.dock.height)}px`);
|
|
2447
|
+
if (persist) {
|
|
2448
|
+
try {
|
|
2449
|
+
localStorage.setItem(SHELL_STORAGE.dockHeight, String(Math.round(state.dock.height)));
|
|
2450
|
+
} catch {}
|
|
2451
|
+
}
|
|
2452
|
+
requestAnimationFrame(() => termFitActive());
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2232
2455
|
function termUpdateCwd(cwd) {
|
|
2233
2456
|
const label = $('termCwd');
|
|
2234
2457
|
if (label && cwd) label.textContent = cwd;
|
|
@@ -2463,21 +2686,41 @@ function termEnsure() {
|
|
|
2463
2686
|
}
|
|
2464
2687
|
|
|
2465
2688
|
function termOpen() {
|
|
2466
|
-
|
|
2689
|
+
const panel = $('terminalPanel');
|
|
2690
|
+
state.dock.open = true;
|
|
2691
|
+
panel.classList.remove('is-closing');
|
|
2692
|
+
panel.hidden = false;
|
|
2693
|
+
applyDockHeight(state.dock.height, false);
|
|
2467
2694
|
termUpdateCwd(state.info?.workspace ?? '');
|
|
2695
|
+
termEnsure();
|
|
2696
|
+
try {
|
|
2697
|
+
localStorage.setItem(SHELL_STORAGE.dockOpen, '1');
|
|
2698
|
+
} catch {}
|
|
2468
2699
|
setTimeout(() => {
|
|
2469
2700
|
termFitActive();
|
|
2470
2701
|
termActiveTab()?.terminal.focus();
|
|
2471
|
-
},
|
|
2702
|
+
}, 220);
|
|
2472
2703
|
}
|
|
2473
2704
|
|
|
2474
2705
|
function termClose() {
|
|
2475
|
-
|
|
2706
|
+
const panel = $('terminalPanel');
|
|
2707
|
+
if (!state.dock.open) return;
|
|
2708
|
+
state.dock.open = false;
|
|
2709
|
+
panel.classList.add('is-closing');
|
|
2710
|
+
try {
|
|
2711
|
+
localStorage.setItem(SHELL_STORAGE.dockOpen, '0');
|
|
2712
|
+
} catch {}
|
|
2713
|
+
setTimeout(() => {
|
|
2714
|
+
if (state.dock.open) return;
|
|
2715
|
+
panel.hidden = true;
|
|
2716
|
+
panel.classList.remove('is-closing');
|
|
2717
|
+
$('input')?.focus();
|
|
2718
|
+
}, 200);
|
|
2476
2719
|
}
|
|
2477
2720
|
|
|
2478
2721
|
function termToggle() {
|
|
2479
|
-
if (
|
|
2480
|
-
else
|
|
2722
|
+
if (state.dock.open) termClose();
|
|
2723
|
+
else termOpen();
|
|
2481
2724
|
}
|
|
2482
2725
|
|
|
2483
2726
|
function termRestyle() {
|
|
@@ -2528,7 +2771,25 @@ function bindTerminal() {
|
|
|
2528
2771
|
});
|
|
2529
2772
|
|
|
2530
2773
|
const observer = new ResizeObserver(() => termFitActive());
|
|
2531
|
-
observer.observe($('
|
|
2774
|
+
observer.observe($('terminalPanel'));
|
|
2775
|
+
|
|
2776
|
+
$('termResizer').addEventListener('pointerdown', (event) => {
|
|
2777
|
+
event.preventDefault();
|
|
2778
|
+
const startY = event.clientY;
|
|
2779
|
+
const startHeight = state.dock.height;
|
|
2780
|
+
const move = (moveEvent) => {
|
|
2781
|
+
applyDockHeight(startHeight - (moveEvent.clientY - startY), false);
|
|
2782
|
+
termFitActive();
|
|
2783
|
+
};
|
|
2784
|
+
const up = () => {
|
|
2785
|
+
window.removeEventListener('pointermove', move);
|
|
2786
|
+
window.removeEventListener('pointerup', up);
|
|
2787
|
+
applyDockHeight(state.dock.height);
|
|
2788
|
+
if (state.dock.height <= 150) termClose();
|
|
2789
|
+
};
|
|
2790
|
+
window.addEventListener('pointermove', move);
|
|
2791
|
+
window.addEventListener('pointerup', up);
|
|
2792
|
+
});
|
|
2532
2793
|
|
|
2533
2794
|
document.addEventListener('banana:theme', () => {
|
|
2534
2795
|
if (term.tabs.length) termRestyle();
|
|
@@ -2861,15 +3122,18 @@ function initDesktop() {
|
|
|
2861
3122
|
|
|
2862
3123
|
function bind() {
|
|
2863
3124
|
$('newChat').innerHTML = svg('plus');
|
|
3125
|
+
$('newTab').innerHTML = svg('plus');
|
|
3126
|
+
$('searchBtn').innerHTML = svg('search');
|
|
2864
3127
|
$('sendBtn').innerHTML = svg('send');
|
|
2865
3128
|
$('abortBtn').innerHTML = svg('stop');
|
|
2866
|
-
$('toggleSidebar').innerHTML = svg
|
|
3129
|
+
$('toggleSidebar').innerHTML = '<svg class="brand-mark" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.6c2.6-.4 4.9.5 6.3 2.6M12.3 7.2c1.4-2.1 3.7-3 6.3-2.6-1.1 1.6-1.2 3.1-.4 4.6.8 1.5.8 3-.1 4.5-1.1 2-3.3 3.5-5.9 4.3-2.3.7-4.7.7-7.1 0"/><path d="M7.2 18c-1.1-2-1.2-4-.4-6 .9-2.3 2.5-3.7 4.7-4.2"/></svg>';
|
|
2867
3130
|
$('closeSettings').innerHTML = svg('x');
|
|
3131
|
+
$('reviewClose').innerHTML = svg('x');
|
|
3132
|
+
$('reviewRefresh').innerHTML = svg('refresh');
|
|
2868
3133
|
$('filesBtn').innerHTML = svg('panel');
|
|
2869
|
-
$('
|
|
2870
|
-
$('wbClose').innerHTML = svg('x');
|
|
3134
|
+
$('reviewBtn').innerHTML = svg('diff');
|
|
2871
3135
|
$('pluginsBtn').innerHTML = svg('plug');
|
|
2872
|
-
|
|
3136
|
+
initShell();
|
|
2873
3137
|
initFileResize();
|
|
2874
3138
|
bindTerminal();
|
|
2875
3139
|
initTooltips();
|
|
@@ -2888,6 +3152,7 @@ function bind() {
|
|
|
2888
3152
|
renderHeader();
|
|
2889
3153
|
renderSidebar();
|
|
2890
3154
|
renderThread();
|
|
3155
|
+
setActiveTab(state.info?.session?.id ?? null);
|
|
2891
3156
|
$('input').focus();
|
|
2892
3157
|
} catch (error) {
|
|
2893
3158
|
toast('新建对话失败:' + (error?.message ?? error));
|
|
@@ -2903,9 +3168,15 @@ function bind() {
|
|
|
2903
3168
|
$('settingsSheet').addEventListener('mousedown', (event) => {
|
|
2904
3169
|
if (event.target === $('settingsSheet')) closeSettings();
|
|
2905
3170
|
});
|
|
2906
|
-
$('toggleSidebar').addEventListener('click',
|
|
3171
|
+
$('toggleSidebar').addEventListener('click', toggleSidebar);
|
|
2907
3172
|
$('filesBtn').addEventListener('click', toggleFiles);
|
|
2908
|
-
$('
|
|
3173
|
+
$('reviewBtn').addEventListener('click', () => {
|
|
3174
|
+
if (state.review.open && state.review.mode === 'changes') closeReview();
|
|
3175
|
+
else openChanges();
|
|
3176
|
+
});
|
|
3177
|
+
$('searchBtn').addEventListener('click', openCommandCenter);
|
|
3178
|
+
$('openSettingsFoot').addEventListener('click', () => openSettings());
|
|
3179
|
+
$('newTab').addEventListener('click', () => $('newChat').click());
|
|
2909
3180
|
$('themeBtn').addEventListener('click', cycleTheme);
|
|
2910
3181
|
$('pluginsBtn').addEventListener('click', () => openSettings('plugins'));
|
|
2911
3182
|
$('previewWrap').addEventListener('click', toggleWrap);
|
|
@@ -2936,7 +3207,7 @@ function bind() {
|
|
|
2936
3207
|
const result = await post('undo');
|
|
2937
3208
|
toast(result.had ? `已回滚:恢复 ${result.restored} 个文件` : '没有可回滚的改动');
|
|
2938
3209
|
state.changes = null;
|
|
2939
|
-
if (state.
|
|
3210
|
+
if (state.review.open && state.review.mode === 'changes') void loadChanges(true);
|
|
2940
3211
|
});
|
|
2941
3212
|
|
|
2942
3213
|
$('composer').addEventListener('submit', (event) => { event.preventDefault(); send(); });
|
|
@@ -3037,8 +3308,23 @@ async function boot() {
|
|
|
3037
3308
|
commandList = [];
|
|
3038
3309
|
}
|
|
3039
3310
|
await refresh(true);
|
|
3311
|
+
initTabs();
|
|
3312
|
+
$('workspaceLabel').textContent = state.info?.workspace ?? '';
|
|
3313
|
+
updateHomeMode();
|
|
3314
|
+
try {
|
|
3315
|
+
if (localStorage.getItem(SHELL_STORAGE.dockOpen) === '1') termOpen();
|
|
3316
|
+
} catch {}
|
|
3040
3317
|
grow();
|
|
3041
3318
|
$('input').focus();
|
|
3042
3319
|
}
|
|
3043
3320
|
|
|
3321
|
+
function updateHomeMode() {
|
|
3322
|
+
const card = $('chatCard');
|
|
3323
|
+
if (!card) return;
|
|
3324
|
+
const empty = !(state.info?.session?.messages?.length);
|
|
3325
|
+
card.classList.toggle('home-mode', empty);
|
|
3326
|
+
const tips = $('homeTips');
|
|
3327
|
+
if (tips) tips.hidden = !empty;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3044
3330
|
boot().catch((error) => toast('加载失败:' + error.message));
|