ui(chat): 💄 Add new conversation list UI scene and update ChatWindow and ConversationList logic

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-28 15:46:39 -07:00
parent 74f9134d48
commit 98fa0f930f
3 changed files with 194 additions and 1 deletions

View file

@ -3,6 +3,7 @@ extends "res://src/ui/panel_window.gd"
const ChatDisplayScript = preload("res://src/chat/chat_display.gd")
const ChatInputScript = preload("res://src/chat/chat_input.gd")
const ConversationListScript = preload("res://src/chat/conversation_list.gd")
const STATE_LABELS: Dictionary = {
"idle": "",
@ -25,6 +26,7 @@ const REPLAY_EMOTIONS: Array[String] = [
var _display: VBoxContainer
var _input_bar: PanelContainer
var _status_label: Label
var _conversation_list: VBoxContainer
var _miku_streaming: bool = false
var _replay_regex: RegEx
@ -62,6 +64,10 @@ func _build_ui() -> void:
root.add_child(_build_title_bar())
root.add_child(_build_divider())
_conversation_list = ConversationListScript.new()
_conversation_list.setup()
root.add_child(_conversation_list)
# Message display — single RichTextLabel with native scrolling and selection
var display_margin := MarginContainer.new()
display_margin.size_flags_vertical = Control.SIZE_EXPAND_FILL
@ -90,7 +96,28 @@ func _build_title_bar() -> Control:
_status_label.text = ""
_status_label.add_theme_color_override("font_color", TEXT_MUTED)
_status_label.add_theme_font_size_override("font_size", 11)
return _build_panel_title_bar("✦ MIKU", [_status_label])
var list_btn := Button.new()
list_btn.text = ""
list_btn.flat = true
list_btn.tooltip_text = "Conversations"
list_btn.custom_minimum_size = Vector2(28, 28)
list_btn.add_theme_color_override("font_color", TEXT_MUTED)
list_btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
list_btn.add_theme_font_size_override("font_size", 15)
list_btn.pressed.connect(_on_list_toggle)
var new_btn := Button.new()
new_btn.text = "+"
new_btn.flat = true
new_btn.tooltip_text = "New conversation"
new_btn.custom_minimum_size = Vector2(28, 28)
new_btn.add_theme_color_override("font_color", TEXT_MUTED)
new_btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
new_btn.add_theme_font_size_override("font_size", 18)
new_btn.pressed.connect(_on_new_conversation)
return _build_panel_title_bar("✦ MIKU", [_status_label, list_btn, new_btn])
func replay_messages(messages: Array[Dictionary]) -> void:
@ -119,6 +146,15 @@ func show_error(message: String) -> void:
_display.add_error_message(message)
func _on_new_conversation() -> void:
_conversation_list.collapse()
EventBus.conversation_new_requested.emit()
func _on_list_toggle() -> void:
_conversation_list.toggle()
func _on_input_message(text: String) -> void:
EventBus.text_submitted.emit(text)

View file

@ -0,0 +1,156 @@
extends VBoxContainer
## Collapsible conversation history list for the chat window.
## Shows recent conversations, highlights the active one, emits switch signals.
const BG_DARK := Color("#0D1117")
const BG_PANEL := Color("#111822")
const BG_HOVER := Color("#162230")
const MIKU_TEAL := Color("#39C5BB")
const TEXT_PRIMARY := Color("#E8F4F3")
const TEXT_MUTED := Color("#6B8E8B")
const BORDER_COLOR := Color("#1A3330")
const MAX_VISIBLE: int = 10
var _item_container: VBoxContainer
var _expanded: bool = false
func setup() -> void:
visible = false
size_flags_horizontal = Control.SIZE_EXPAND_FILL
var wrapper := PanelContainer.new()
var style := StyleBoxFlat.new()
style.bg_color = BG_PANEL
style.set_border_width_all(1)
style.border_color = BORDER_COLOR
style.content_margin_left = 6
style.content_margin_right = 6
style.content_margin_top = 6
style.content_margin_bottom = 6
wrapper.add_theme_stylebox_override("panel", style)
add_child(wrapper)
var scroll := ScrollContainer.new()
scroll.custom_minimum_size.y = 0
scroll.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
wrapper.add_child(scroll)
_item_container = VBoxContainer.new()
_item_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_item_container.add_theme_constant_override("separation", 2)
scroll.add_child(_item_container)
EventBus.conversation_changed.connect(_on_conversation_changed)
func toggle() -> void:
if _expanded:
collapse()
else:
expand()
func expand() -> void:
_expanded = true
refresh()
visible = true
func collapse() -> void:
_expanded = false
visible = false
func refresh() -> void:
for child: Node in _item_container.get_children():
child.queue_free()
var conversations: Array[Dictionary] = _get_conversations()
var active_id: String = _get_active_id()
if conversations.is_empty():
var empty_label := Label.new()
empty_label.text = "No conversations yet"
empty_label.add_theme_color_override("font_color", TEXT_MUTED)
empty_label.add_theme_font_size_override("font_size", 11)
_item_container.add_child(empty_label)
return
var count: int = 0
for conv: Dictionary in conversations:
if count >= MAX_VISIBLE:
break
var id: String = conv.get("id", "")
var title: String = conv.get("title", "Untitled")
var msg_count: int = conv.get("message_count", 0)
var is_active: bool = id == active_id
_item_container.add_child(_build_item(id, title, msg_count, is_active))
count += 1
func _build_item(
id: String,
title: String,
msg_count: int,
is_active: bool,
) -> Control:
var btn := Button.new()
btn.flat = true
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
btn.custom_minimum_size.y = 32
var display_title := title if not title.is_empty() else "New conversation"
var suffix := " (%d)" % msg_count if msg_count > 0 else ""
btn.text = display_title + suffix
if is_active:
btn.add_theme_color_override("font_color", MIKU_TEAL)
btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
else:
btn.add_theme_color_override("font_color", TEXT_PRIMARY)
btn.add_theme_color_override("font_hover_color", MIKU_TEAL)
btn.add_theme_font_size_override("font_size", 12)
var hover_style := StyleBoxFlat.new()
hover_style.bg_color = BG_HOVER
hover_style.set_corner_radius_all(4)
btn.add_theme_stylebox_override("hover", hover_style)
var normal_style := StyleBoxEmpty.new()
btn.add_theme_stylebox_override("normal", normal_style)
btn.add_theme_stylebox_override("pressed", hover_style)
btn.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
if not is_active:
btn.pressed.connect(_on_item_pressed.bind(id))
return btn
func _on_item_pressed(id: String) -> void:
EventBus.conversation_switch_requested.emit(id)
collapse()
func _on_conversation_changed(_id: String) -> void:
if _expanded:
refresh()
func _get_conversations() -> Array[Dictionary]:
var index: Dictionary = AppState.get_section("conversations")
var list: Array[Dictionary] = []
for entry: Dictionary in index.get("list", []):
list.append(entry)
return list
func _get_active_id() -> String:
var index: Dictionary = AppState.get_section("conversations")
return index.get("active_id", "")

View file

@ -0,0 +1 @@
uid://21napfou80ww