diff infinite_canvas/canvas.c @ 280:49e9e591c9bb

Add persistent dictation, prewarmed WebRTC speech input, Copilot SDK routing, animated conversation lifecycle controls, parking, and architecture coverage.
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 19:14:53 -0700
parents 8d560f50ed4c
children c57149ad216e
line wrap: on
line diff
--- a/infinite_canvas/canvas.c	Mon Aug 17 22:22:36 2026 -0700
+++ b/infinite_canvas/canvas.c	Tue Aug 18 19:14:53 2026 -0700
@@ -38,6 +38,10 @@
 #define CANVAS_TEXT_AREA_FONT_SIZE 16.0f
 #define CANVAS_TEXT_AREA_SPACING 0.1f
 #define CANVAS_TEXT_AREA_LINE_HEIGHT 22.0f
+#define CANVAS_CONVERSATION_EXPANDED_HEIGHT 300.0f
+#define CANVAS_CONVERSATION_COLLAPSED_HEIGHT 58.0f
+#define CANVAS_PARKING_LOT_X 2400.0f
+#define CANVAS_PARKING_LOT_Y -600.0f
 
 typedef struct {
     int32 start;
@@ -46,6 +50,13 @@
     float width;
 } Canvas_Text_Line;
 
+static int32 Canvas_Text_Build_Lines(
+    Font font,
+    const char *p_text,
+    float max_width,
+    Canvas_Text_Line *p_lines,
+    int32 line_capacity);
+
 static float Canvas_Clamp(float value, float min_value, float max_value)
 {
     if (value < min_value) return min_value;
@@ -743,6 +754,28 @@
     };
 }
 
+static Rectangle Canvas_Conversation_Collapse_Bounds(
+    const Canvas_Entity *p_entity)
+{
+    return (Rectangle){
+        p_entity->position.x + p_entity->size.x - 74.0f,
+        p_entity->position.y + 12.0f,
+        28.0f,
+        28.0f,
+    };
+}
+
+static Rectangle Canvas_Conversation_Park_Bounds(
+    const Canvas_Entity *p_entity)
+{
+    return (Rectangle){
+        p_entity->position.x + p_entity->size.x - 40.0f,
+        p_entity->position.y + 12.0f,
+        28.0f,
+        28.0f,
+    };
+}
+
 static Rectangle Canvas_Entity_Bounds(const Canvas_Entity *p_entity)
 {
     if (p_entity->type == CANVAS_ENTITY_CIRCLE) {
@@ -753,6 +786,7 @@
             p_entity->size.x * 2.0f,
         };
     }
+
     if (p_entity->type == CANVAS_ENTITY_LINE) {
         float end_x = p_entity->position.x + p_entity->size.x;
         float end_y = p_entity->position.y + p_entity->size.y;
@@ -949,6 +983,16 @@
             Canvas_Context_Append(p_writer, "url=");
             Canvas_Context_Append_Quoted(p_writer, p_entity->text);
             break;
+        case CANVAS_ENTITY_CONVERSATION:
+            Canvas_Context_Append(p_writer, "title=");
+            Canvas_Context_Append_Quoted(p_writer, p_entity->label);
+            Canvas_Context_Append(p_writer, " transcript=");
+            Canvas_Context_Append_Quoted(p_writer, p_entity->text);
+            Canvas_Context_Append(
+                p_writer,
+                " collapsed=%s",
+                p_entity->active ? "true" : "false");
+            break;
         case CANVAS_ENTITY_LUCIDE_GALLERY:
             Canvas_Context_Append(
                 p_writer,
@@ -997,6 +1041,14 @@
             p_entity->animation_amount <= 0.01f) {
             continue;
         }
+        if (p_entity->type == CANVAS_ENTITY_TEXT_AREA &&
+            strcmp(p_entity->label, "Dictation") == 0) {
+            continue;
+        }
+        if (p_entity->type == CANVAS_ENTITY_CONVERSATION &&
+            p_entity->parked) {
+            continue;
+        }
         if (!CheckCollisionRecs(viewport, Canvas_Entity_Bounds(p_entity))) continue;
 
         snapshot.visible_count++;
@@ -1124,6 +1176,14 @@
         entity.active = TRUE;
         snprintf(entity.text, sizeof(entity.text), "https://mrjunejune.com");
     }
+    if (type == CANVAS_ENTITY_CONVERSATION) {
+        entity.size = (Vector2){
+            320.0f,
+            CANVAS_CONVERSATION_EXPANDED_HEIGHT,
+        };
+        entity.animation_amount = 1.0f;
+        snprintf(entity.label, sizeof(entity.label), "Agent session");
+    }
     if (type == CANVAS_ENTITY_LUCIDE_GALLERY) {
         entity.size = (Vector2){1440.0f, 1040.0f};
     }
@@ -1132,6 +1192,188 @@
     return TRUE;
 }
 
+uint32 Canvas_Scene_Add_Conversation(
+    Canvas_Scene *p_scene,
+    const char *p_title,
+    const char *p_prompt,
+    const char *p_response,
+    Vector2 position)
+{
+    if (!Canvas_Scene_Add(
+            p_scene,
+            CANVAS_ENTITY_CONVERSATION,
+            position)) {
+        return 0;
+    }
+    Canvas_Entity *p_entity =
+        &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1];
+    p_entity->size = (Vector2){420.0f, 300.0f};
+    snprintf(
+        p_entity->label,
+        sizeof(p_entity->label),
+        "%s",
+        p_title && p_title[0] ? p_title : "New agent session");
+    snprintf(
+        p_entity->text,
+        sizeof(p_entity->text),
+        "You\n%s\n\nCopilot\n%s",
+        p_prompt,
+        p_response);
+    p_scene->selected_index =
+        (int32)Dowa_Array_Length(p_scene->p_entities) - 1;
+    return p_entity->id;
+}
+
+boolean Canvas_Scene_Append_Conversation(
+    Canvas_Scene *p_scene,
+    uint32 entity_id,
+    const char *p_prompt,
+    const char *p_response)
+{
+    for (size_t index = 0;
+        index < Dowa_Array_Length(p_scene->p_entities);
+        index++) {
+        Canvas_Entity *p_entity = &p_scene->p_entities[index];
+        if (p_entity->id != entity_id ||
+            p_entity->type != CANVAS_ENTITY_CONVERSATION) {
+            continue;
+        }
+        size_t length = strlen(p_entity->text);
+        snprintf(
+            p_entity->text + length,
+            sizeof(p_entity->text) - length,
+            "\n\nYou\n%s\n\nCopilot\n%s",
+            p_prompt,
+            p_response);
+        p_scene->selected_index = (int32)index;
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static float Canvas_Conversation_Max_Scroll(
+    const Canvas_Entity *p_entity,
+    Font font)
+{
+    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
+    int32 line_count = Canvas_Text_Build_Lines(
+        font,
+        p_entity->text,
+        p_entity->size.x - 32.0f,
+        lines,
+        CANVAS_ENTITY_TEXT_CAPACITY);
+    return fmaxf(
+        0.0f,
+        (float)line_count * 18.0f -
+            (CANVAS_CONVERSATION_EXPANDED_HEIGHT - 82.0f));
+}
+
+void Canvas_Conversation_Scroll_To_End(
+    Canvas_Entity *p_entity,
+    Font font)
+{
+    if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return;
+    p_entity->value = (int32)Canvas_Conversation_Max_Scroll(p_entity, font);
+}
+
+void Canvas_Conversation_Set_Collapsed(
+    Canvas_Entity *p_entity,
+    boolean collapsed)
+{
+    if (!p_entity || p_entity->type != CANVAS_ENTITY_CONVERSATION) return;
+    p_entity->active = collapsed;
+}
+
+size_t Canvas_Scene_Parked_Conversation_Count(
+    const Canvas_Scene *p_scene)
+{
+    size_t count = 0;
+    for (size_t index = 0;
+        index < Dowa_Array_Length(p_scene->p_entities);
+        index++) {
+        const Canvas_Entity *p_entity = &p_scene->p_entities[index];
+        if (p_entity->type == CANVAS_ENTITY_CONVERSATION &&
+            p_entity->parked) {
+            count++;
+        }
+    }
+    return count;
+}
+
+boolean Canvas_Scene_Set_Conversation_Parked(
+    Canvas_Scene *p_scene,
+    uint32 entity_id,
+    boolean parked)
+{
+    Canvas_Entity *p_entity = NULL;
+    for (size_t index = 0;
+        index < Dowa_Array_Length(p_scene->p_entities);
+        index++) {
+        if (p_scene->p_entities[index].id == entity_id &&
+            p_scene->p_entities[index].type == CANVAS_ENTITY_CONVERSATION) {
+            p_entity = &p_scene->p_entities[index];
+            break;
+        }
+    }
+    if (!p_entity || p_entity->parked == parked) return FALSE;
+    if (parked) {
+        size_t slot = Canvas_Scene_Parked_Conversation_Count(p_scene);
+        p_entity->parked_restore_position = p_entity->position;
+        p_entity->position = (Vector2){
+            CANVAS_PARKING_LOT_X + (float)(slot % 3) * 460.0f,
+            CANVAS_PARKING_LOT_Y + (float)(slot / 3) * 340.0f,
+        };
+        p_entity->parked = TRUE;
+        p_entity->pinned = FALSE;
+    } else {
+        p_entity->position = p_entity->parked_restore_position;
+        p_entity->parked = FALSE;
+    }
+    return TRUE;
+}
+
+boolean Canvas_Scene_Show_Parking_Lot(
+    Canvas_Scene *p_scene,
+    Canvas_Camera *p_camera)
+{
+    Rectangle bounds = {0};
+    boolean found = FALSE;
+    for (size_t index = 0;
+        index < Dowa_Array_Length(p_scene->p_entities);
+        index++) {
+        Canvas_Entity *p_entity = &p_scene->p_entities[index];
+        if (p_entity->type != CANVAS_ENTITY_CONVERSATION ||
+            !p_entity->parked) {
+            continue;
+        }
+        Rectangle entity_bounds = Canvas_Entity_Bounds(p_entity);
+        if (!found) {
+            bounds = entity_bounds;
+            found = TRUE;
+        } else {
+            float right = fmaxf(
+                bounds.x + bounds.width,
+                entity_bounds.x + entity_bounds.width);
+            float bottom = fmaxf(
+                bounds.y + bounds.height,
+                entity_bounds.y + entity_bounds.height);
+            bounds.x = fminf(bounds.x, entity_bounds.x);
+            bounds.y = fminf(bounds.y, entity_bounds.y);
+            bounds.width = right - bounds.x;
+            bounds.height = bottom - bounds.y;
+        }
+    }
+    if (!found) return FALSE;
+    Canvas_Camera_Fit_Bounds(
+        p_camera,
+        bounds,
+        80.0f,
+        80.0f,
+        80.0f,
+        80.0f);
+    return TRUE;
+}
+
 void Canvas_Scene_Clear(Canvas_Scene *p_scene)
 {
     Dowa_Array_Clear(p_scene->p_entities);
@@ -1205,6 +1447,18 @@
 
     Canvas_Scene_Add(p_scene, CANVAS_ENTITY_IMAGE, (Vector2){-550.0f, 220.0f});
     Canvas_Scene_Add(p_scene, CANVAS_ENTITY_WEB_CONTENT, (Vector2){-150.0f, 120.0f});
+    Canvas_Scene_Add(p_scene, CANVAS_ENTITY_CONVERSATION, (Vector2){610.0f, 220.0f});
+    Canvas_Entity *p_conversation =
+        &p_scene->p_entities[Dowa_Array_Length(p_scene->p_entities) - 1];
+    snprintf(
+        p_conversation->label,
+        sizeof(p_conversation->label),
+        "Spatial agent sessions");
+    snprintf(
+        p_conversation->text,
+        sizeof(p_conversation->text),
+        "System\nPress M to dictate and Enter to send, or select the text "
+        "area and press Ctrl/Cmd+Enter to ask Copilot.");
 }
 
 void Canvas_Scene_Add_Browser_Grid(Canvas_Scene *p_scene)
@@ -1322,7 +1576,8 @@
         case CANVAS_ENTITY_SWITCH:
         case CANVAS_ENTITY_TABLE:
         case CANVAS_ENTITY_SCROLL_AREA:
-        case CANVAS_ENTITY_IMAGE: {
+        case CANVAS_ENTITY_IMAGE:
+        case CANVAS_ENTITY_CONVERSATION: {
             float height = p_entity->size.y;
             if (p_entity->type == CANVAS_ENTITY_DROPDOWN && p_entity->active) {
                 height += 8.0f + 3.0f * 40.0f;
@@ -1613,11 +1868,12 @@
 static Rectangle Canvas_Text_Area_Content_Bounds(
     const Canvas_Entity *p_entity)
 {
+    float header_height = p_entity->label[0] ? 26.0f : 0.0f;
     return (Rectangle){
         p_entity->position.x + CANVAS_TEXT_AREA_PADDING,
-        p_entity->position.y + CANVAS_TEXT_AREA_PADDING,
+        p_entity->position.y + CANVAS_TEXT_AREA_PADDING + header_height,
         p_entity->size.x - CANVAS_TEXT_AREA_PADDING * 2.0f,
-        p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f,
+        p_entity->size.y - CANVAS_TEXT_AREA_PADDING * 2.0f - header_height,
     };
 }
 
@@ -1798,6 +2054,7 @@
         p_entity->text_scroll_y + content.height) {
         p_entity->text_scroll_y = cursor_bottom - content.height;
     }
+
     float max_scroll = fmaxf(
         0.0f,
         (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height);
@@ -1807,6 +2064,31 @@
         max_scroll);
 }
 
+void Canvas_Text_Area_Set_Content(
+    Canvas_Entity *p_entity,
+    Font font,
+    const char *p_text,
+    boolean muted)
+{
+    if (!p_entity || p_entity->type != CANVAS_ENTITY_TEXT_AREA) return;
+    snprintf(p_entity->text, sizeof(p_entity->text), "%s", p_text ? p_text : "");
+    p_entity->text_cursor = (int32)strlen(p_entity->text);
+    p_entity->text_selection_anchor = p_entity->text_cursor;
+    p_entity->text_muted = muted;
+
+    Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
+    Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
+    int32 line_count = Canvas_Text_Build_Lines(
+        font,
+        p_entity->text,
+        content.width,
+        lines,
+        CANVAS_ENTITY_TEXT_CAPACITY);
+    p_entity->text_scroll_y = fmaxf(
+        0.0f,
+        (float)line_count * CANVAS_TEXT_AREA_LINE_HEIGHT - content.height);
+}
+
 static void Canvas_Text_Update_Keyboard(Canvas_Entity *p_entity, Font font)
 {
     boolean shortcut = Canvas_Control_Is_Down();
@@ -2130,11 +2412,14 @@
     for (size_t index = 0; index < Dowa_Array_Length(p_scene->p_entities); index++) {
         Canvas_Entity *p_entity = &p_scene->p_entities[index];
         if (p_entity->type != CANVAS_ENTITY_ACCORDION &&
-            p_entity->type != CANVAS_ENTITY_SWITCH) {
+            p_entity->type != CANVAS_ENTITY_SWITCH &&
+            p_entity->type != CANVAS_ENTITY_CONVERSATION) {
             continue;
         }
 
-        float target = p_entity->active ? 1.0f : 0.0f;
+        float target = p_entity->type == CANVAS_ENTITY_CONVERSATION
+            ? (p_entity->active ? 0.0f : 1.0f)
+            : (p_entity->active ? 1.0f : 0.0f);
         float speed = p_entity->type == CANVAS_ENTITY_SWITCH ? 7.0f : 4.5f;
         float step = GetFrameTime() * speed;
         if (p_entity->animation_amount < target) {
@@ -2149,6 +2434,21 @@
         float amount = p_entity->animation_amount;
         float eased = amount * amount * (3.0f - 2.0f * amount);
         if (p_entity->type == CANVAS_ENTITY_SWITCH) continue;
+        if (p_entity->type == CANVAS_ENTITY_CONVERSATION) {
+            float height = CANVAS_CONVERSATION_COLLAPSED_HEIGHT +
+                (CANVAS_CONVERSATION_EXPANDED_HEIGHT -
+                    CANVAS_CONVERSATION_COLLAPSED_HEIGHT) * eased;
+            if (p_entity->pinned) {
+                float pinned_scale =
+                    p_entity->pinned_screen_size.x / p_entity->size.x;
+                p_entity->pinned_screen_size.y = height * pinned_scale;
+                p_entity->size.y =
+                    p_entity->pinned_screen_size.y / p_camera->zoom;
+            } else {
+                p_entity->size.y = height;
+            }
+            continue;
+        }
         if (p_entity->pinned) {
             float pinned_scale = p_entity->pinned_screen_size.x / 300.0f;
             p_entity->pinned_screen_size.y =
@@ -2217,6 +2517,27 @@
                     240.0f);
                 consumed_scroll = TRUE;
             }
+        } else if (p_hovered->type == CANVAS_ENTITY_CONVERSATION &&
+            !control_down) {
+            float wheel = GetMouseWheelMove();
+            if (wheel != 0.0f) {
+                Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
+                int32 line_count = Canvas_Text_Build_Lines(
+                    font,
+                    p_hovered->text,
+                    p_hovered->size.x - 32.0f,
+                    lines,
+                    CANVAS_ENTITY_TEXT_CAPACITY);
+                float max_scroll = fmaxf(
+                    0.0f,
+                    (float)line_count * 18.0f -
+                        (p_hovered->size.y - 82.0f));
+                p_hovered->value = (int32)Canvas_Clamp(
+                    (float)p_hovered->value - wheel * 36.0f,
+                    0.0f,
+                    max_scroll);
+                consumed_scroll = TRUE;
+            }
         } else if (p_hovered->type == CANVAS_ENTITY_LUCIDE_GALLERY &&
             !control_down) {
             float wheel = GetMouseWheelMove();
@@ -2256,6 +2577,27 @@
 
         if (p_scene->selected_index >= 0) {
             Canvas_Entity *p_entity = &p_scene->p_entities[p_scene->selected_index];
+            if (p_entity->type == CANVAS_ENTITY_CONVERSATION &&
+                CheckCollisionPointRec(
+                    world_pointer,
+                    Canvas_Conversation_Park_Bounds(p_entity))) {
+                Canvas_Scene_Set_Conversation_Parked(
+                    p_scene,
+                    p_entity->id,
+                    p_entity->parked ? FALSE : TRUE);
+                p_scene->pressed_index = -1;
+                return TRUE;
+            }
+            if (p_entity->type == CANVAS_ENTITY_CONVERSATION &&
+                CheckCollisionPointRec(
+                    world_pointer,
+                    Canvas_Conversation_Collapse_Bounds(p_entity))) {
+                Canvas_Conversation_Set_Collapsed(
+                    p_entity,
+                    p_entity->active ? FALSE : TRUE);
+                p_scene->pressed_index = -1;
+                return TRUE;
+            }
             if (p_entity->type == CANVAS_ENTITY_LUCIDE_GALLERY) {
                 p_entity->active = CheckCollisionPointRec(
                     world_pointer,
@@ -2676,7 +3018,8 @@
         case CANVAS_ENTITY_TABLE:
         case CANVAS_ENTITY_NOTIFICATION:
         case CANVAS_ENTITY_SCROLL_AREA:
-        case CANVAS_ENTITY_IMAGE: {
+        case CANVAS_ENTITY_IMAGE:
+        case CANVAS_ENTITY_CONVERSATION: {
             DrawRectangleRoundedLinesEx(
                 (Rectangle){
                     p_entity->position.x - padding,
@@ -2751,13 +3094,23 @@
 {
     Rectangle content = Canvas_Text_Area_Content_Bounds(p_entity);
     if (!p_entity->text[0]) {
+        boolean dictation = strcmp(p_entity->label, "Dictation") == 0;
         DrawTextEx(
             font,
-            "Write something...",
+            dictation ? "Speak now..." : "Type a thought for Copilot...",
             (Vector2){content.x, content.y},
             CANVAS_TEXT_AREA_FONT_SIZE,
             CANVAS_TEXT_AREA_SPACING,
             p_theme->text_muted);
+        DrawTextEx(
+            font,
+            dictation
+                ? "Enter to send  |  Backspace to clear"
+                : "Ctrl/Cmd+Enter to send",
+            (Vector2){content.x, content.y + 22.0f},
+            12.0f,
+            0.0f,
+            Canvas_Color_Fade(p_theme->text_muted, 0.72f));
         if (p_entity->active && fmod(GetTime(), 1.0) < 0.55) {
             DrawLineEx(
                 (Vector2){content.x, content.y},
@@ -2840,7 +3193,7 @@
             (Vector2){content.x, y},
             CANVAS_TEXT_AREA_FONT_SIZE,
             CANVAS_TEXT_AREA_SPACING,
-            p_theme->text);
+            p_entity->text_muted ? p_theme->text_muted : p_theme->text);
 
         if (p_entity->active &&
             line == cursor_line &&
@@ -3012,6 +3365,29 @@
                 14,
                 (p_entity->active ? 2.0f : 1.0f) / zoom,
                 p_entity->active ? p_theme->accent : p_theme->border);
+            if (p_entity->label[0]) {
+                DrawTextEx(
+                    font,
+                    p_entity->label,
+                    (Vector2){
+                        bounds.x + CANVAS_TEXT_AREA_PADDING,
+                        bounds.y + 11.0f,
+                    },
+                    12.0f,
+                    0.0f,
+                    p_theme->text_muted);
+                DrawLineEx(
+                    (Vector2){
+                        bounds.x + bounds.width - 38.0f,
+                        bounds.y + 17.0f,
+                    },
+                    (Vector2){
+                        bounds.x + bounds.width - 16.0f,
+                        bounds.y + 17.0f,
+                    },
+                    2.0f / zoom,
+                    p_theme->border);
+            }
             Canvas_Draw_Text_Area_Content(p_entity, font, p_theme);
             break;
         }
@@ -3505,6 +3881,122 @@
 #endif
             break;
         }
+        case CANVAS_ENTITY_CONVERSATION: {
+            Rectangle bounds = {
+                p_entity->position.x,
+                p_entity->position.y,
+                p_entity->size.x,
+                p_entity->size.y,
+            };
+            Canvas_Theme_Draw_Shadow(
+                p_theme,
+                bounds,
+                0.06f,
+                14,
+                zoom,
+                1.0f);
+            DrawRectangleRounded(bounds, 0.06f, 14, p_theme->surface);
+            DrawRectangleRoundedLinesEx(
+                bounds,
+                0.06f,
+                14,
+                1.0f / zoom,
+                p_theme->border);
+            DrawRectangleRounded(
+                (Rectangle){bounds.x, bounds.y, bounds.width, 54.0f},
+                0.06f,
+                14,
+                p_theme->surface_muted);
+            Canvas_Lucide_Draw_Icon(
+                Canvas_Lucide_Find_Icon(
+                    p_entity->agent_working ? "sparkles" : "messages-square"),
+                (Vector2){bounds.x + 16.0f, bounds.y + 15.0f},
+                0.82f,
+                1.8f,
+                p_entity->agent_working ?
+                    p_theme->accent :
+                    p_theme->text);
+            DrawTextEx(
+                font,
+                p_entity->label,
+                (Vector2){bounds.x + 48.0f, bounds.y + 12.0f},
+                17.0f,
+                0.0f,
+                p_theme->text);
+            DrawTextEx(
+                font,
+                p_entity->parked
+                    ? "Parked session"
+                    : (p_entity->agent_working
+                        ? "Thinking..."
+                        : "Agent session"),
+                (Vector2){bounds.x + 48.0f, bounds.y + 33.0f},
+                11.0f,
+                0.0f,
+                p_theme->text_muted);
+            Rectangle collapse =
+                Canvas_Conversation_Collapse_Bounds(p_entity);
+            Rectangle park = Canvas_Conversation_Park_Bounds(p_entity);
+            Canvas_Lucide_Draw_Icon(
+                Canvas_Lucide_Find_Icon(
+                    p_entity->active ? "chevron-down" : "chevron-up"),
+                (Vector2){collapse.x + 5.0f, collapse.y + 5.0f},
+                0.68f,
+                1.6f,
+                p_theme->text_muted);
+            Canvas_Lucide_Draw_Icon(
+                Canvas_Lucide_Find_Icon(
+                    p_entity->parked ? "archive-restore" : "archive"),
+                (Vector2){park.x + 5.0f, park.y + 5.0f},
+                0.68f,
+                1.6f,
+                p_theme->text_muted);
+
+            if (p_entity->size.y <= 82.0f) break;
+            Rectangle content = {
+                bounds.x + 16.0f,
+                bounds.y + 68.0f,
+                bounds.width - 32.0f,
+                bounds.height - 82.0f,
+            };
+            Canvas_Text_Line lines[CANVAS_ENTITY_TEXT_CAPACITY];
+            int32 line_count = Canvas_Text_Build_Lines(
+                font,
+                p_entity->text,
+                content.width,
+                lines,
+                CANVAS_ENTITY_TEXT_CAPACITY);
+            for (int32 line_index = 0;
+                line_index < line_count;
+                line_index++) {
+                float y = content.y + (float)line_index * 18.0f -
+                    (float)p_entity->value;
+                if (y + 18.0f < content.y ||
+                    y > content.y + content.height) {
+                    continue;
+                }
+                char line_text[CANVAS_ENTITY_TEXT_CAPACITY];
+                int32 length =
+                    lines[line_index].end - lines[line_index].start;
+                memcpy(
+                    line_text,
+                    p_entity->text + lines[line_index].start,
+                    (size_t)length);
+                line_text[length] = '\0';
+                boolean speaker =
+                    strcmp(line_text, "You") == 0 ||
+                    strcmp(line_text, "Copilot") == 0 ||
+                    strcmp(line_text, "System") == 0;
+                DrawTextEx(
+                    font,
+                    line_text,
+                    (Vector2){content.x, y},
+                    14.0f,
+                    0.0f,
+                    speaker ? p_theme->accent : p_theme->text);
+            }
+            break;
+        }
         case CANVAS_ENTITY_LUCIDE_GALLERY: {
             Rectangle bounds = {
                 p_entity->position.x,
@@ -4046,6 +4538,7 @@
         case CANVAS_ENTITY_SCROLL_AREA: return "Scrollable area";
         case CANVAS_ENTITY_IMAGE: return "Image";
         case CANVAS_ENTITY_WEB_CONTENT: return "Web content";
+        case CANVAS_ENTITY_CONVERSATION: return "Agent conversation";
         case CANVAS_ENTITY_LUCIDE_GALLERY: return "Lucide gallery";
         default: return "Unknown";
     }