comparison infinite_canvas/main.c @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -0700
parents 49e9e591c9bb
children
comparison
equal deleted inserted replaced
280:49e9e591c9bb 281:c57149ad216e
1 #include "infinite_canvas/agent_response.h"
1 #include "infinite_canvas/agent_service.h" 2 #include "infinite_canvas/agent_service.h"
2 #include "infinite_canvas/canvas.h" 3 #include "infinite_canvas/canvas.h"
4 #include "infinite_canvas/scene_store.h"
3 #include "infinite_canvas/web_surface.h" 5 #include "infinite_canvas/web_surface.h"
4 6
5 #if defined(INFINITE_CANVAS_DEV_UI) 7 #if defined(INFINITE_CANVAS_DEV_UI)
6 #include "infinite_canvas/dev_ui.h" 8 #include "infinite_canvas/dev_ui.h"
7 #endif 9 #endif
25 Canvas_Theme theme; 27 Canvas_Theme theme;
26 boolean owns_font; 28 boolean owns_font;
27 const char *p_screenshot_path; 29 const char *p_screenshot_path;
28 uint32 frame_count; 30 uint32 frame_count;
29 uint32 screenshot_frame; 31 uint32 screenshot_frame;
30 uint32 pending_conversation_id; 32 Vector2 pending_indicator_position;
33 Vector2 pending_indicator_drag_offset;
31 uint32 dictation_entity_id; 34 uint32 dictation_entity_id;
32 boolean agent_initialized; 35 boolean agent_initialized;
36 boolean pending_indicator_visible;
37 boolean pending_indicator_hovered;
38 boolean pending_indicator_dragging;
39 float pending_indicator_hover_amount;
33 boolean dictation_overlay_visible; 40 boolean dictation_overlay_visible;
34 boolean dictation_listening; 41 boolean dictation_listening;
35 boolean dictation_requested_active; 42 boolean dictation_requested_active;
36 boolean dictation_send_pending; 43 boolean dictation_send_pending;
37 char dictation_status[128]; 44 char dictation_status[128];
44 #endif 51 #endif
45 } Canvas_App; 52 } Canvas_App;
46 53
47 static Canvas_App *g_app = NULL; 54 static Canvas_App *g_app = NULL;
48 55
49 static const char *Canvas_App_JSON_String(
50 const char *p_json,
51 const char *p_key,
52 char *p_output,
53 size_t output_capacity)
54 {
55 char needle[128];
56 snprintf(needle, sizeof(needle), "\"%s\"", p_key);
57 const char *p_cursor = strstr(p_json, needle);
58 if (!p_cursor) return NULL;
59 p_cursor = strchr(p_cursor + strlen(needle), ':');
60 if (!p_cursor) return NULL;
61 while (*++p_cursor == ' ') {}
62 if (*p_cursor != '"') return NULL;
63 p_cursor++;
64 size_t output = 0;
65 while (*p_cursor && *p_cursor != '"' && output + 1 < output_capacity) {
66 if (*p_cursor == '\\' && p_cursor[1]) {
67 p_cursor++;
68 if (*p_cursor == 'n') p_output[output++] = '\n';
69 else if (*p_cursor == 't') p_output[output++] = '\t';
70 else if (*p_cursor != 'r') p_output[output++] = *p_cursor;
71 } else {
72 p_output[output++] = *p_cursor;
73 }
74 p_cursor++;
75 }
76 p_output[output] = '\0';
77 return p_output;
78 }
79
80 static uint32 Canvas_App_JSON_Uint(const char *p_json, const char *p_key)
81 {
82 char needle[128];
83 snprintf(needle, sizeof(needle), "\"%s\"", p_key);
84 const char *p_cursor = strstr(p_json, needle);
85 if (!p_cursor) return 0;
86 p_cursor = strchr(p_cursor + strlen(needle), ':');
87 if (!p_cursor) return 0;
88 return (uint32)strtoul(p_cursor + 1, NULL, 10);
89 }
90
91 static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id) 56 static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id)
92 { 57 {
93 for (size_t index = 0; 58 for (size_t index = 0;
94 index < Dowa_Array_Length(g_app->scene.p_entities); 59 index < Dowa_Array_Length(g_app->scene.p_entities);
95 index++) { 60 index++) {
96 if (g_app->scene.p_entities[index].id == entity_id) { 61 if (g_app->scene.p_entities[index].id == entity_id) {
97 return &g_app->scene.p_entities[index]; 62 return &g_app->scene.p_entities[index];
98 } 63 }
99 } 64 }
100 return NULL; 65 return NULL;
101 }
102
103 static void Canvas_App_Set_Conversation_Text(
104 Canvas_Entity *p_entity,
105 const char *p_prompt,
106 const char *p_speaker,
107 const char *p_response)
108 {
109 size_t length = 0;
110 int32 written = snprintf(
111 p_entity->text,
112 sizeof(p_entity->text),
113 "You\n");
114 if (written > 0) length = (size_t)written;
115 written = snprintf(
116 p_entity->text + length,
117 sizeof(p_entity->text) - length,
118 "%.*s\n\n%s\n",
119 900,
120 p_prompt,
121 p_speaker);
122 if (written > 0) length += (size_t)written;
123 if (length >= sizeof(p_entity->text)) {
124 length = sizeof(p_entity->text) - 1;
125 }
126 snprintf(
127 p_entity->text + length,
128 sizeof(p_entity->text) - length,
129 "%.*s",
130 900,
131 p_response);
132 Canvas_Conversation_Scroll_To_End(p_entity, g_app->font);
133 } 66 }
134 67
135 static boolean Canvas_App_Submit_Prompt(const char *p_prompt) 68 static boolean Canvas_App_Submit_Prompt(const char *p_prompt)
136 { 69 {
137 if (!p_prompt || !p_prompt[0]) return FALSE; 70 if (!p_prompt || !p_prompt[0]) return FALSE;
149 snprintf( 82 snprintf(
150 g_app->pending_prompt, 83 g_app->pending_prompt,
151 sizeof(g_app->pending_prompt), 84 sizeof(g_app->pending_prompt),
152 "%s", 85 "%s",
153 p_prompt); 86 p_prompt);
154 Vector2 position = Canvas_Camera_Screen_To_World( 87 g_app->pending_indicator_position = Canvas_Camera_Screen_To_World(
155 &g_app->camera, 88 &g_app->camera,
156 (Vector2){ 89 (Vector2){
157 (float)g_app->camera.viewport_width * 0.5f - 210.0f, 90 (float)g_app->camera.viewport_width * 0.5f + 240.0f,
158 (float)g_app->camera.viewport_height * 0.5f - 150.0f, 91 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
159 }); 92 });
160 g_app->pending_conversation_id = Canvas_Scene_Add_Conversation( 93 g_app->pending_indicator_visible = TRUE;
161 &g_app->scene, 94 g_app->pending_indicator_hovered = FALSE;
162 "Routing thought...", 95 g_app->pending_indicator_dragging = FALSE;
163 p_prompt, 96 g_app->pending_indicator_hover_amount = 0.0f;
164 "Copilot is deciding whether to create or extend a session.",
165 position);
166 Canvas_Entity *p_pending =
167 Canvas_App_Find_Entity(g_app->pending_conversation_id);
168 if (p_pending) p_pending->agent_working = TRUE;
169 return TRUE; 97 return TRUE;
170 } 98 }
171 99
100 static uint32 Canvas_App_Materialize_Agent_Entities(
101 Canvas_Agent_Response *p_response)
102 {
103 uint32 created = 0;
104 for (uint32 index = 0; index < p_response->entity_count; index++) {
105 Canvas_Agent_Entity_Spec *p_spec = &p_response->entities[index];
106 Vector2 position = p_spec->has_position
107 ? p_spec->position
108 : (Vector2){
109 g_app->pending_indicator_position.x +
110 (float)(index % 2) * 280.0f,
111 g_app->pending_indicator_position.y +
112 (float)(index / 2) * 240.0f,
113 };
114 if (!Canvas_Scene_Add(&g_app->scene, p_spec->type, position)) {
115 break;
116 }
117 Canvas_Entity *p_entity = &g_app->scene.p_entities[
118 Dowa_Array_Length(g_app->scene.p_entities) - 1];
119 if (p_spec->label[0]) {
120 snprintf(
121 p_entity->label,
122 sizeof(p_entity->label),
123 "%s",
124 p_spec->label);
125 }
126 if (p_spec->text[0]) {
127 snprintf(
128 p_entity->text,
129 sizeof(p_entity->text),
130 "%s",
131 p_spec->text);
132 p_entity->text_cursor = (int32)strlen(p_entity->text);
133 p_entity->text_selection_anchor = p_entity->text_cursor;
134 }
135 if (p_spec->has_size) p_entity->size = p_spec->size;
136 if (p_spec->has_value) p_entity->value = p_spec->value;
137 created++;
138 }
139 return created;
140 }
141
142 static void Canvas_App_Show_Agent_Notification(
143 const char *p_title,
144 const char *p_text)
145 {
146 if (!Canvas_Scene_Add(
147 &g_app->scene,
148 CANVAS_ENTITY_NOTIFICATION,
149 g_app->pending_indicator_position)) {
150 return;
151 }
152 Canvas_Entity *p_entity = &g_app->scene.p_entities[
153 Dowa_Array_Length(g_app->scene.p_entities) - 1];
154 if (p_title && p_title[0]) {
155 snprintf(
156 p_entity->label,
157 sizeof(p_entity->label),
158 "%s",
159 p_title);
160 }
161 snprintf(
162 p_entity->text,
163 sizeof(p_entity->text),
164 "%s",
165 p_text ? p_text : "");
166 }
167
172 static void Canvas_App_Apply_Agent_Response(const char *p_json) 168 static void Canvas_App_Apply_Agent_Response(const char *p_json)
173 { 169 {
174 char action[32] = {0}; 170 Canvas_Agent_Response parsed;
175 char title[128] = {0}; 171 boolean valid = Canvas_Agent_Response_Parse(p_json, &parsed);
176 char response[CANVAS_ENTITY_TEXT_CAPACITY] = {0}; 172 if (!valid) {
177 Canvas_App_JSON_String(p_json, "action", action, sizeof(action)); 173 memset(&parsed, 0, sizeof(parsed));
178 Canvas_App_JSON_String(p_json, "title", title, sizeof(title)); 174 snprintf(parsed.action, sizeof(parsed.action), "create");
179 if (!Canvas_App_JSON_String( 175 snprintf(
180 p_json, 176 parsed.presentation,
181 "response", 177 sizeof(parsed.presentation),
182 response, 178 "conversation");
183 sizeof(response))) { 179 snprintf(
184 snprintf(response, sizeof(response), "%s", p_json); 180 parsed.response,
185 } 181 sizeof(parsed.response),
186 uint32 conversation_id = 182 "%s",
187 Canvas_App_JSON_Uint(p_json, "conversation_id"); 183 p_json);
188 Canvas_Entity *p_pending = 184 }
189 Canvas_App_Find_Entity(g_app->pending_conversation_id); 185 uint32 created_entities =
190 if (strcmp(action, "append") == 0 && 186 Canvas_App_Materialize_Agent_Entities(&parsed);
191 conversation_id != 0 && 187 if (strcmp(parsed.action, "append") == 0 &&
188 parsed.conversation_id != 0 &&
192 Canvas_Scene_Append_Conversation( 189 Canvas_Scene_Append_Conversation(
193 &g_app->scene, 190 &g_app->scene,
194 conversation_id, 191 parsed.conversation_id,
195 g_app->pending_prompt, 192 g_app->pending_prompt,
196 response)) { 193 parsed.response)) {
197 Canvas_Entity *p_conversation = 194 Canvas_Entity *p_conversation =
198 Canvas_App_Find_Entity(conversation_id); 195 Canvas_App_Find_Entity(parsed.conversation_id);
199 Canvas_Conversation_Scroll_To_End( 196 Canvas_Conversation_Scroll_To_End(
200 p_conversation, 197 p_conversation,
201 g_app->font); 198 g_app->font);
202 if (p_pending) p_pending->removing = TRUE; 199 } else if (strcmp(parsed.presentation, "action") == 0) {
203 } else if (p_pending) { 200 if (created_entities == 0) {
204 snprintf( 201 Canvas_App_Show_Agent_Notification(
205 p_pending->label, 202 parsed.title[0] ? parsed.title : "Copilot",
206 sizeof(p_pending->label), 203 parsed.response);
207 "%s", 204 }
208 title[0] ? title : "Copilot session");
209 Canvas_App_Set_Conversation_Text(
210 p_pending,
211 g_app->pending_prompt,
212 "Copilot",
213 response);
214 p_pending->agent_working = FALSE;
215 } else { 205 } else {
216 Vector2 position = Canvas_Camera_Screen_To_World( 206 Vector2 position = {
217 &g_app->camera, 207 g_app->pending_indicator_position.x - 210.0f,
218 (Vector2){ 208 g_app->pending_indicator_position.y - 150.0f,
219 (float)g_app->camera.viewport_width * 0.5f - 210.0f, 209 };
220 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
221 });
222 uint32 created_id = Canvas_Scene_Add_Conversation( 210 uint32 created_id = Canvas_Scene_Add_Conversation(
223 &g_app->scene, 211 &g_app->scene,
224 title[0] ? title : "Copilot session", 212 parsed.title[0] ? parsed.title : "Copilot session",
225 g_app->pending_prompt, 213 g_app->pending_prompt,
226 response, 214 parsed.response,
227 position); 215 position);
228 Canvas_Conversation_Scroll_To_End( 216 Canvas_Conversation_Scroll_To_End(
229 Canvas_App_Find_Entity(created_id), 217 Canvas_App_Find_Entity(created_id),
230 g_app->font); 218 g_app->font);
231 } 219 }
232 g_app->pending_conversation_id = 0; 220 g_app->pending_indicator_visible = FALSE;
233 g_app->pending_prompt[0] = '\0'; 221 g_app->pending_prompt[0] = '\0';
234 } 222 }
235 223
236 static Canvas_Entity *Canvas_App_Ensure_Dictation_Entity(void) 224 static Canvas_Entity *Canvas_App_Ensure_Dictation_Entity(void)
237 { 225 {
419 14.0f, 407 14.0f,
420 0.0f, 408 0.0f,
421 g_app->theme.text_muted); 409 g_app->theme.text_muted);
422 } 410 }
423 411
412 static void Canvas_App_Draw_Agent_Indicator(void)
413 {
414 if (!g_app->pending_indicator_visible) return;
415 Vector2 screen = Canvas_Camera_World_To_Screen(
416 &g_app->camera,
417 g_app->pending_indicator_position);
418 float pulse = 0.5f + 0.5f * sinf((float)GetTime() * 5.0f);
419 float hover = g_app->pending_indicator_hover_amount;
420 float radius = 22.0f + hover * 2.0f;
421 Rectangle bounds = {
422 screen.x - radius,
423 screen.y - radius,
424 radius * 2.0f,
425 radius * 2.0f,
426 };
427 Canvas_Theme_Draw_Shadow(
428 &g_app->theme,
429 bounds,
430 0.5f,
431 16,
432 1.0f,
433 1.0f);
434 DrawCircleV(screen, radius, g_app->theme.surface);
435 DrawCircleLinesV(
436 screen,
437 20.0f + pulse * 2.0f,
438 Fade(g_app->theme.accent, 0.45f + pulse * 0.35f));
439 Canvas_Draw_Lucide_Icon(
440 "sparkles",
441 (Vector2){screen.x - 9.0f, screen.y - 9.0f},
442 0.75f,
443 1.8f,
444 g_app->theme.accent);
445 if (hover > 0.01f) {
446 Rectangle label = {
447 screen.x + 28.0f - (1.0f - hover) * 8.0f,
448 screen.y - 17.0f,
449 118.0f,
450 34.0f,
451 };
452 DrawRectangleRounded(
453 label,
454 0.35f,
455 10,
456 Fade(g_app->theme.surface, hover));
457 DrawRectangleRoundedLinesEx(
458 label,
459 0.35f,
460 10,
461 1.0f,
462 Fade(g_app->theme.border, hover));
463 DrawTextEx(
464 g_app->font,
465 g_app->pending_indicator_dragging
466 ? "Move result"
467 : "Copilot working",
468 (Vector2){label.x + 12.0f, label.y + 10.0f},
469 13.0f,
470 0.0f,
471 Fade(g_app->theme.text, hover));
472 }
473 }
474
475 static boolean Canvas_App_Update_Agent_Indicator_Input(boolean blocked)
476 {
477 if (!g_app->pending_indicator_visible) {
478 g_app->pending_indicator_hovered = FALSE;
479 g_app->pending_indicator_dragging = FALSE;
480 g_app->pending_indicator_hover_amount = 0.0f;
481 return FALSE;
482 }
483 Vector2 pointer = GetMousePosition();
484 Vector2 screen = Canvas_Camera_World_To_Screen(
485 &g_app->camera,
486 g_app->pending_indicator_position);
487 if (g_app->pending_indicator_dragging) {
488 Vector2 target_screen = {
489 pointer.x - g_app->pending_indicator_drag_offset.x,
490 pointer.y - g_app->pending_indicator_drag_offset.y,
491 };
492 g_app->pending_indicator_position =
493 Canvas_Camera_Screen_To_World(
494 &g_app->camera,
495 target_screen);
496 if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
497 g_app->pending_indicator_dragging = FALSE;
498 }
499 g_app->pending_indicator_hovered = TRUE;
500 } else {
501 g_app->pending_indicator_hovered =
502 !blocked &&
503 CheckCollisionPointCircle(pointer, screen, 26.0f);
504 if (g_app->pending_indicator_hovered &&
505 IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
506 g_app->pending_indicator_dragging = TRUE;
507 g_app->pending_indicator_drag_offset = (Vector2){
508 pointer.x - screen.x,
509 pointer.y - screen.y,
510 };
511 }
512 }
513 float target = g_app->pending_indicator_hovered ? 1.0f : 0.0f;
514 float step = GetFrameTime() * 10.0f;
515 if (g_app->pending_indicator_hover_amount < target) {
516 g_app->pending_indicator_hover_amount = fminf(
517 target,
518 g_app->pending_indicator_hover_amount + step);
519 } else {
520 g_app->pending_indicator_hover_amount = fmaxf(
521 target,
522 g_app->pending_indicator_hover_amount - step);
523 }
524 return g_app->pending_indicator_hovered ||
525 g_app->pending_indicator_dragging;
526 }
527
528 #if defined(INFINITE_CANVAS_DEV_UI)
529 static void Canvas_App_Handle_Dev_Action(Canvas_Dev_UI_Action action)
530 {
531 if (action == CANVAS_DEV_UI_ACTION_NONE) return;
532 char path[CANVAS_SCENE_STORE_PATH_CAPACITY];
533 if (!Canvas_Scene_Store_Path(
534 g_app->dev_ui.snapshot_name,
535 path,
536 sizeof(path))) {
537 Canvas_Dev_UI_Set_Snapshot_Status(
538 &g_app->dev_ui,
539 "Use only letters, numbers, - or _");
540 return;
541 }
542 if (action == CANVAS_DEV_UI_ACTION_SAVE_SNAPSHOT) {
543 Canvas_Dev_UI_Set_Snapshot_Status(
544 &g_app->dev_ui,
545 Canvas_Scene_Store_Save(
546 path,
547 &g_app->scene,
548 &g_app->camera)
549 ? "Snapshot saved"
550 : "Snapshot save failed");
551 return;
552 }
553 if (g_app->agent_initialized &&
554 g_app->agent_service.status == CANVAS_AGENT_WORKING) {
555 Canvas_Dev_UI_Set_Snapshot_Status(
556 &g_app->dev_ui,
557 "Wait for the active agent turn before loading");
558 return;
559 }
560 if (!Canvas_Scene_Store_Load(
561 path,
562 &g_app->scene,
563 &g_app->camera)) {
564 Canvas_Dev_UI_Set_Snapshot_Status(
565 &g_app->dev_ui,
566 "Snapshot missing, incompatible, or corrupt");
567 return;
568 }
569 g_app->pending_indicator_visible = FALSE;
570 g_app->pending_prompt[0] = '\0';
571 g_app->dictation_entity_id = 0;
572 g_app->dictation_transcript[0] = '\0';
573 g_app->dictation_partial[0] = '\0';
574 for (size_t index = 0;
575 index < Dowa_Array_Length(g_app->scene.p_entities);
576 index++) {
577 Canvas_Entity *p_entity = &g_app->scene.p_entities[index];
578 if (p_entity->type == CANVAS_ENTITY_TEXT_AREA &&
579 strcmp(p_entity->label, "Dictation") == 0) {
580 g_app->dictation_entity_id = p_entity->id;
581 snprintf(
582 p_entity->text_muted
583 ? g_app->dictation_partial
584 : g_app->dictation_transcript,
585 CANVAS_ENTITY_TEXT_CAPACITY,
586 "%s",
587 p_entity->text);
588 break;
589 }
590 }
591 g_app->dictation_overlay_visible =
592 g_app->dictation_transcript[0] ||
593 g_app->dictation_partial[0];
594 Canvas_Dev_UI_Set_Snapshot_Status(
595 &g_app->dev_ui,
596 "Snapshot loaded");
597 }
598 #endif
599
424 static void Canvas_App_Draw_Frame(void) 600 static void Canvas_App_Draw_Frame(void)
425 { 601 {
426 int32 width = GetScreenWidth(); 602 int32 width = GetScreenWidth();
427 int32 height = GetScreenHeight(); 603 int32 height = GetScreenHeight();
428 Canvas_Camera_Set_Viewport(&g_app->camera, width, height); 604 Canvas_Camera_Set_Viewport(&g_app->camera, width, height);
429 605
430 boolean block_pointer_input = FALSE; 606 boolean block_pointer_input = FALSE;
607 boolean dev_ui_editing = FALSE;
431 #if defined(INFINITE_CANVAS_DEV_UI) 608 #if defined(INFINITE_CANVAS_DEV_UI)
432 block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui); 609 block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui);
433 #endif 610 dev_ui_editing = Canvas_Dev_UI_Is_Editing(&g_app->dev_ui);
611 #endif
612 block_pointer_input =
613 Canvas_App_Update_Agent_Indicator_Input(block_pointer_input) ||
614 block_pointer_input;
434 Canvas_Web_Surface_Set_Dark_Mode( 615 Canvas_Web_Surface_Set_Dark_Mode(
435 &g_app->web_surface, 616 &g_app->web_surface,
436 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE); 617 g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE);
437 boolean dictation_hotkey = 618 boolean dictation_hotkey =
619 !dev_ui_editing &&
438 !Canvas_Scene_Is_Text_Editing(&g_app->scene) && 620 !Canvas_Scene_Is_Text_Editing(&g_app->scene) &&
439 !Canvas_Web_Chrome_Is_Editing(&g_app->scene) && 621 !Canvas_Web_Chrome_Is_Editing(&g_app->scene) &&
440 IsKeyPressed(KEY_M); 622 IsKeyPressed(KEY_M);
441 if (dictation_hotkey) { 623 if (dictation_hotkey) {
442 Canvas_App_Set_Dictation_Active( 624 Canvas_App_Set_Dictation_Active(
494 g_app->font, 676 g_app->font,
495 block_pointer_input); 677 block_pointer_input);
496 boolean keyboard_focus = 678 boolean keyboard_focus =
497 Canvas_Scene_Is_Text_Editing(&g_app->scene) || 679 Canvas_Scene_Is_Text_Editing(&g_app->scene) ||
498 Canvas_Web_Chrome_Is_Editing(&g_app->scene) || 680 Canvas_Web_Chrome_Is_Editing(&g_app->scene) ||
681 dev_ui_editing ||
499 Canvas_Web_Surface_Is_Focused(&g_app->web_surface); 682 Canvas_Web_Surface_Is_Focused(&g_app->web_surface);
500 if (!keyboard_focus && IsKeyPressed(KEY_P)) { 683 if (!keyboard_focus && IsKeyPressed(KEY_P)) {
501 Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera); 684 Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera);
502 } 685 }
503 if (!keyboard_focus && 686 if (!keyboard_focus &&
612 sizeof(agent_error)) : 795 sizeof(agent_error)) :
613 CANVAS_AGENT_IDLE; 796 CANVAS_AGENT_IDLE;
614 if (agent_status == CANVAS_AGENT_READY) { 797 if (agent_status == CANVAS_AGENT_READY) {
615 Canvas_App_Apply_Agent_Response(agent_response); 798 Canvas_App_Apply_Agent_Response(agent_response);
616 } else if (agent_status == CANVAS_AGENT_ERROR) { 799 } else if (agent_status == CANVAS_AGENT_ERROR) {
617 Canvas_Entity *p_pending = 800 if (g_app->pending_prompt[0]) {
618 Canvas_App_Find_Entity(g_app->pending_conversation_id); 801 Canvas_App_Show_Agent_Notification(
619 if (p_pending) { 802 "Copilot unavailable",
620 snprintf(p_pending->label, sizeof(p_pending->label), "Copilot unavailable");
621 Canvas_App_Set_Conversation_Text(
622 p_pending,
623 g_app->pending_prompt,
624 "System",
625 agent_error); 803 agent_error);
626 p_pending->agent_working = FALSE; 804 }
627 } else if (g_app->pending_prompt[0]) { 805 g_app->pending_indicator_visible = FALSE;
628 Vector2 position = Canvas_Camera_Screen_To_World(
629 &g_app->camera,
630 (Vector2){
631 (float)g_app->camera.viewport_width * 0.5f - 210.0f,
632 (float)g_app->camera.viewport_height * 0.5f - 150.0f,
633 });
634 Canvas_Scene_Add_Conversation(
635 &g_app->scene,
636 "Copilot unavailable",
637 g_app->pending_prompt,
638 agent_error,
639 position);
640 }
641 g_app->pending_conversation_id = 0;
642 g_app->pending_prompt[0] = '\0'; 806 g_app->pending_prompt[0] = '\0';
643 } 807 }
644 808
645 Canvas_Web_Surface_Sync(&g_app->web_surface, &g_app->camera, &g_app->scene); 809 Canvas_Web_Surface_Sync(&g_app->web_surface, &g_app->camera, &g_app->scene);
646 810
685 Canvas_Draw_World_Overlays( 849 Canvas_Draw_World_Overlays(
686 &g_app->camera, 850 &g_app->camera,
687 &g_app->scene, 851 &g_app->scene,
688 g_app->font, 852 g_app->font,
689 &g_app->theme); 853 &g_app->theme);
854 Canvas_App_Draw_Agent_Indicator();
690 #if defined(INFINITE_CANVAS_DEV_UI) 855 #if defined(INFINITE_CANVAS_DEV_UI)
691 Canvas_Dev_UI_Draw( 856 Canvas_Dev_UI_Action dev_action = Canvas_Dev_UI_Draw(
692 &g_app->dev_ui, 857 &g_app->dev_ui,
693 &g_app->camera, 858 &g_app->camera,
694 &g_app->scene, 859 &g_app->scene,
695 g_app->font, 860 g_app->font,
696 &g_app->theme); 861 &g_app->theme);
697 #endif 862 #endif
698 Canvas_App_Draw_Listening_Indicator(); 863 Canvas_App_Draw_Listening_Indicator();
699 EndDrawing(); 864 EndDrawing();
865 #if defined(INFINITE_CANVAS_DEV_UI)
866 Canvas_App_Handle_Dev_Action(dev_action);
867 #endif
700 g_app->frame_count++; 868 g_app->frame_count++;
701 #if !defined(PLATFORM_WEB) 869 #if !defined(PLATFORM_WEB)
702 if (g_app->frame_count == 30) { 870 if (g_app->frame_count == 30) {
703 RestoreWindow(); 871 RestoreWindow();
704 SetWindowFocused(); 872 SetWindowFocused();