From 931a0488cc9ef7b7e3877c41151313c53c168ad1 Mon Sep 17 00:00:00 2001
From: JOJO <1498581755@qq.com>
Date: Wed, 26 Nov 2025 20:00:11 +0800
Subject: [PATCH] feat: restyle utility panel and streaming focus
---
core/web_terminal.py | 2 +-
static/icons/layers.svg | 1 +
static/src/App.vue | 8 +++--
static/src/app.ts | 6 ++++
static/src/components/panels/FocusPanel.vue | 21 ++++++++++++-
static/src/components/panels/LeftPanel.vue | 31 ++++++++++++++++---
.../sidebar/ConversationSidebar.vue | 18 +++++++++++
static/src/stores/ui.ts | 8 +++++
.../styles/components/chat/_chat-area.scss | 12 +++++++
.../components/panels/_focus-panel.scss | 31 ++++++++++++-------
.../styles/components/panels/_left-panel.scss | 21 +++++++++++++
.../components/sidebar/_conversation.scss | 28 +++++++++++++++++
static/src/styles/layout/_app-shell.scss | 8 +++++
static/src/utils/icons.ts | 1 +
sub_agent/core/web_terminal.py | 2 +-
sub_agent/web_server.py | 10 +++++-
web_server.py | 13 ++++++--
17 files changed, 197 insertions(+), 24 deletions(-)
create mode 100644 static/icons/layers.svg
diff --git a/core/web_terminal.py b/core/web_terminal.py
index ffbfa6e..255d538 100644
--- a/core/web_terminal.py
+++ b/core/web_terminal.py
@@ -536,7 +536,7 @@ class WebTerminal(MainTerminal):
# 如果是聚焦操作,广播聚焦文件更新
- if tool_name in ['focus_file', 'unfocus_file', 'modify_file']:
+ if tool_name in ['focus_file', 'unfocus_file', 'modify_file', 'append_to_file']:
try:
focused_files_dict = self.get_focused_files_info()
self.broadcast('focused_files_update', focused_files_dict)
diff --git a/static/icons/layers.svg b/static/icons/layers.svg
new file mode 100644
index 0000000..a36c416
--- /dev/null
+++ b/static/icons/layers.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/src/App.vue b/static/src/App.vue
index 433972a..1b93db5 100644
--- a/static/src/App.vue
+++ b/static/src/App.vue
@@ -37,10 +37,11 @@
-
+
-
+
diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue
index 490c5d8..c371875 100644
--- a/static/src/components/panels/LeftPanel.vue
+++ b/static/src/components/panels/LeftPanel.vue
@@ -1,5 +1,10 @@
-
diff --git a/static/src/stores/ui.ts b/static/src/stores/ui.ts
index f2a2064..65dde47 100644
--- a/static/src/stores/ui.ts
+++ b/static/src/stores/ui.ts
@@ -48,6 +48,7 @@ interface EasterEggState {
interface UiState {
sidebarCollapsed: boolean;
+ utilityPanelHidden: boolean;
panelMode: PanelMode;
panelMenuOpen: boolean;
leftWidth: number;
@@ -69,6 +70,7 @@ interface UiState {
export const useUiStore = defineStore('ui', {
state: (): UiState => ({
sidebarCollapsed: true,
+ utilityPanelHidden: false,
panelMode: 'files',
panelMenuOpen: false,
leftWidth: 350,
@@ -98,6 +100,12 @@ export const useUiStore = defineStore('ui', {
setSidebarCollapsed(collapsed: boolean) {
this.sidebarCollapsed = collapsed;
},
+ setUtilityPanelHidden(hidden: boolean) {
+ this.utilityPanelHidden = hidden;
+ },
+ toggleUtilityPanelHidden() {
+ this.utilityPanelHidden = !this.utilityPanelHidden;
+ },
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss
index 5d05775..b6bce77 100644
--- a/static/src/styles/components/chat/_chat-area.scss
+++ b/static/src/styles/components/chat/_chat-area.scss
@@ -22,6 +22,18 @@
position: relative;
}
+.main-container.utility-panel-hidden .messages-area {
+ max-width: 960px;
+ margin: 0 auto;
+ width: 100%;
+ padding-left: clamp(24px, 6vw, 72px);
+ padding-right: clamp(24px, 6vw, 72px);
+}
+
+.main-container.utility-panel-hidden .message-block {
+ width: 100%;
+}
+
.chat-container .input-area {
position: absolute;
left: 0;
diff --git a/static/src/styles/components/panels/_focus-panel.scss b/static/src/styles/components/panels/_focus-panel.scss
index 9bfaa03..61759f8 100644
--- a/static/src/styles/components/panels/_focus-panel.scss
+++ b/static/src/styles/components/panels/_focus-panel.scss
@@ -1,13 +1,17 @@
/* 聚焦文件 */
.focused-files {
padding: 16px;
+ background: rgba(255, 255, 255, 0.4);
}
-o-files {
+.no-files {
text-align: center;
color: var(--claude-text-secondary);
padding: 60px 20px;
font-size: 14px;
+ background: #fff;
+ border-radius: 12px;
+ border: 1px dashed var(--claude-border);
}
.file-tabs {
@@ -17,21 +21,21 @@ o-files {
}
.file-tab {
- border: 1px solid var(--claude-border);
- border-radius: 12px;
+ border: 1px solid rgba(118, 103, 84, 0.15);
+ border-radius: 14px;
overflow: hidden;
- background: rgba(255, 255, 255, 0.75);
- box-shadow: 0 12px 28px rgba(61, 57, 41, 0.08);
+ background: #fff;
+ box-shadow: 0 10px 24px rgba(61, 57, 41, 0.08);
}
.tab-header {
- background: rgba(218, 119, 86, 0.08);
- padding: 10px 16px;
+ background: rgba(247, 244, 240, 0.9);
+ padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
- border-bottom: 1px solid var(--claude-border);
+ border-bottom: 1px solid rgba(118, 103, 84, 0.12);
}
.file-name {
@@ -45,19 +49,22 @@ o-files {
}
.file-content {
- max-height: 320px;
+ max-height: 340px;
overflow-y: auto;
- background: #1e1e1e;
+ background: #fff;
}
.file-content pre {
margin: 0;
- padding: 16px;
+ padding: 16px 20px;
+ background: transparent;
}
.file-content code {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
font-size: 13px;
line-height: 1.5;
- color: #aed581;
+ color: #2c2c2c;
+ white-space: pre-wrap;
+ word-break: break-word;
}
diff --git a/static/src/styles/components/panels/_left-panel.scss b/static/src/styles/components/panels/_left-panel.scss
index 45581f6..12e5861 100644
--- a/static/src/styles/components/panels/_left-panel.scss
+++ b/static/src/styles/components/panels/_left-panel.scss
@@ -23,6 +23,24 @@
.sidebar.left-sidebar {
display: flex;
flex-direction: column;
+ transition:
+ width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
+ min-width 0.35s cubic-bezier(0.4, 0, 0.2, 1),
+ flex-basis 0.35s cubic-bezier(0.4, 0, 0.2, 1),
+ transform 0.35s ease,
+ opacity 0.35s ease;
+}
+
+.sidebar.left-sidebar.panel-hidden {
+ opacity: 0;
+ transform: translateY(-80px);
+ pointer-events: none;
+}
+
+.sidebar.left-sidebar.panel-hidden .sidebar-status,
+.sidebar.left-sidebar.panel-hidden .sidebar-panel-card-wrapper {
+ opacity: 0;
+ transition: opacity 0.2s ease;
}
.sidebar-status {
@@ -86,6 +104,8 @@
color: #fffef8;
box-shadow: 0 8px 20px rgba(189, 93, 58, 0.25);
transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
+ border: none;
+ cursor: pointer;
}
.mode-indicator.fast {
@@ -224,6 +244,7 @@
flex-direction: column;
overflow: hidden;
min-height: 0;
+ overflow-x: hidden;
}
.sidebar-panel-card .sidebar-header {
diff --git a/static/src/styles/components/sidebar/_conversation.scss b/static/src/styles/components/sidebar/_conversation.scss
index 9aa79ab..ceb9167 100644
--- a/static/src/styles/components/sidebar/_conversation.scss
+++ b/static/src/styles/components/sidebar/_conversation.scss
@@ -142,6 +142,34 @@
stroke-linejoin: round;
}
+.utility-panel-toggle-btn {
+ width: 48px;
+ height: 48px;
+ border-radius: 24px;
+ border: none;
+ background: transparent;
+ color: #2f251b;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.25s ease, transform 0.2s ease;
+ padding: 0;
+}
+
+.utility-panel-toggle-btn .icon {
+ --icon-size: 22px;
+ display: inline-flex;
+}
+
+.utility-panel-toggle-btn:hover,
+.utility-panel-toggle-btn:focus-visible {
+ color: var(--claude-accent);
+}
+
+.utility-panel-toggle-btn.active {
+ color: #2f251b;
+}
+
.new-conversation-btn {
flex: 1;
background: linear-gradient(135deg, var(--claude-accent) 0%, var(--claude-accent-strong) 100%);
diff --git a/static/src/styles/layout/_app-shell.scss b/static/src/styles/layout/_app-shell.scss
index cd35061..bb78207 100644
--- a/static/src/styles/layout/_app-shell.scss
+++ b/static/src/styles/layout/_app-shell.scss
@@ -9,6 +9,14 @@
color: var(--claude-text);
}
+.main-container.utility-panel-hidden {
+ background: var(--claude-bg);
+}
+
+.main-container.utility-panel-hidden .chat-container {
+ background: rgba(255, 255, 255, 0.92);
+}
+
#app {
min-height: var(--app-viewport, 100vh);
background: var(--claude-bg);
diff --git a/static/src/utils/icons.ts b/static/src/utils/icons.ts
index 2e87f02..c4f9d19 100644
--- a/static/src/utils/icons.ts
+++ b/static/src/utils/icons.ts
@@ -17,6 +17,7 @@ export const ICONS = Object.freeze({
hammer: '/static/icons/hammer.svg',
info: '/static/icons/info.svg',
laptop: '/static/icons/laptop.svg',
+ layers: '/static/icons/layers.svg',
menu: '/static/icons/menu.svg',
monitor: '/static/icons/monitor.svg',
octagon: '/static/icons/octagon.svg',
diff --git a/sub_agent/core/web_terminal.py b/sub_agent/core/web_terminal.py
index c5a22f9..9cb0c02 100644
--- a/sub_agent/core/web_terminal.py
+++ b/sub_agent/core/web_terminal.py
@@ -525,7 +525,7 @@ class WebTerminal(MainTerminal):
# 如果是聚焦操作,广播聚焦文件更新
- if tool_name in ['focus_file', 'unfocus_file', 'modify_file']:
+ if tool_name in ['focus_file', 'unfocus_file', 'modify_file', 'append_to_file']:
try:
focused_files_dict = self.get_focused_files_info()
self.broadcast('focused_files_update', focused_files_dict)
diff --git a/sub_agent/web_server.py b/sub_agent/web_server.py
index 8ad8921..b896247 100644
--- a/sub_agent/web_server.py
+++ b/sub_agent/web_server.py
@@ -2757,6 +2757,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
+ try:
+ sender('focused_files_update', web_terminal.get_focused_files_info())
+ except Exception as focus_exc:
+ debug_log(f"广播聚焦文件更新失败: {focus_exc}")
debug_log(f"追加写入完成: {summary}")
else:
@@ -3108,6 +3112,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
+ try:
+ sender('focused_files_update', web_terminal.get_focused_files_info())
+ except Exception as focus_exc:
+ debug_log(f"广播聚焦文件更新失败: {focus_exc}")
pending_modify = None
modify_probe_buffer = ""
@@ -4093,7 +4101,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
sender('update_action', update_payload)
# 更新UI状态
- if function_name in ['focus_file', 'unfocus_file', 'modify_file']:
+ if function_name in ['focus_file', 'unfocus_file', 'modify_file', 'append_to_file']:
sender('focused_files_update', web_terminal.get_focused_files_info())
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']:
diff --git a/web_server.py b/web_server.py
index 67c8eb4..1db6502 100644
--- a/web_server.py
+++ b/web_server.py
@@ -2662,6 +2662,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
+ try:
+ sender('focused_files_update', web_terminal.get_focused_files_info())
+ except Exception as focus_exc:
+ debug_log(f"广播聚焦文件更新失败: {focus_exc}")
debug_log(f"追加写入完成: {summary}")
else:
@@ -2961,7 +2965,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
summary_parts.append(apply_result["error"])
matching_note = "提示:补丁匹配基于完整文本,包含注释和空白符,请确保 <<>> 段落与文件内容逐字一致。如果修改成功,请忽略,如果失败,请明确原文后再次尝试。"
- summary_parts.append(matching_note)
+ if failed_blocks or not completed_blocks:
+ summary_parts.append(matching_note)
summary_message = " ".join(summary_parts).strip()
result["summary_message"] = summary_message
result["success"] = bool(completed_blocks) and not failed_blocks and apply_result.get("error") is None
@@ -3013,6 +3018,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
if refreshed.get("success"):
web_terminal.focused_files[path] = refreshed["content"]
debug_log(f"聚焦文件已刷新: {path}")
+ try:
+ sender('focused_files_update', web_terminal.get_focused_files_info())
+ except Exception as focus_exc:
+ debug_log(f"广播聚焦文件更新失败: {focus_exc}")
pending_modify = None
modify_probe_buffer = ""
@@ -4089,7 +4098,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
sender('update_action', update_payload)
# 更新UI状态
- if function_name in ['focus_file', 'unfocus_file', 'modify_file']:
+ if function_name in ['focus_file', 'unfocus_file', 'modify_file', 'append_to_file']:
sender('focused_files_update', web_terminal.get_focused_files_info())
if function_name in ['create_file', 'delete_file', 'rename_file', 'create_folder']: