Mercurial
changeset 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 | |
| files | infinite_canvas/BUILD infinite_canvas/README.md infinite_canvas/agent_response.c infinite_canvas/agent_response.h infinite_canvas/agent_response_test.c infinite_canvas/agent_service_copilot.c infinite_canvas/canvas.c infinite_canvas/canvas.h infinite_canvas/canvas_test.c infinite_canvas/dev_ui.c infinite_canvas/dev_ui.h infinite_canvas/docs/agent-sessions.md infinite_canvas/docs/entities-and-components.md infinite_canvas/main.c infinite_canvas/scene_store.c infinite_canvas/scene_store.h infinite_canvas/scene_store_test.c mrjunejune/inference/copilot_sidecar.py mrjunejune/inference/copilot_sidecar_test.py |
| diffstat | 19 files changed, 1716 insertions(+), 224 deletions(-) [+] |
line wrap: on
line diff
--- a/infinite_canvas/BUILD Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/BUILD Tue Aug 18 22:18:15 2026 -0700 @@ -41,12 +41,24 @@ hdrs = ["dev_ui.h"], deps = [ ":canvas", + ":scene_store", ":theme", "//third_party/raylib:raygui", ], ) cc_library( + name = "scene_store", + srcs = ["scene_store.c"], + hdrs = ["scene_store.h"], + deps = [ + ":canvas", + "//dowa:dowa", + "//third_party/raylib:raylib", + ], +) + +cc_library( name = "web_surface", srcs = select({ "//config:linux": ["web_surface_native.cc"], @@ -98,6 +110,16 @@ }), ) +cc_library( + name = "agent_response", + srcs = ["agent_response.c"], + hdrs = ["agent_response.h"], + deps = [ + ":canvas", + "//dowa:dowa", + ], +) + filegroup( name = "inter_font", srcs = ["assets/Inter-Variable.ttf"], @@ -147,8 +169,10 @@ "//conditions:default": [], }), deps = [ + ":agent_response", ":agent_service", ":canvas", + ":scene_store", ":web_surface", "//dowa:dowa", "//third_party/raylib:raylib", @@ -177,9 +201,11 @@ }), defines = ["INFINITE_CANVAS_DEV_UI"], deps = [ + ":agent_response", ":agent_service", ":canvas", ":dev_ui", + ":scene_store", ":theme", ":web_surface", "//dowa:dowa", @@ -258,9 +284,11 @@ ], defines = ["INFINITE_CANVAS_DEV_UI"], deps = [ + ":agent_response", ":agent_service", ":canvas", ":dev_ui", + ":scene_store", ":web_surface", "//dowa:dowa", "//third_party/raylib:raylib", @@ -297,6 +325,12 @@ ) cc_test( + name = "scene_store_test", + srcs = ["scene_store_test.c"], + deps = [":scene_store"], +) + +cc_test( name = "agent_service_test", srcs = ["agent_service_test.c"], args = [ @@ -316,6 +350,12 @@ }), ) +cc_test( + name = "agent_response_test", + srcs = ["agent_response_test.c"], + deps = [":agent_response"], +) + sh_test( name = "dictation_policy_test", srcs = ["dictation_policy_test.sh"],
--- a/infinite_canvas/README.md Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/README.md Tue Aug 18 22:18:15 2026 -0700 @@ -147,9 +147,21 @@ Developer Controls jumps there, and the restore icon returns the session to its previous board position. Collapse and expansion use the same eased retained animation model as accordion entities rather than snapping between heights. -Canvas startup also pre-creates the persistent orchestration SDK session and -three worker sessions, moving SDK session creation out of the first submitted -thought. +Canvas startup pre-creates only the persistent orchestration SDK session, +moving its setup out of the first submitted thought without eagerly creating +worker sessions. + +Developer Controls can save and load named native `.zmap` snapshots. The +versioned binary representation restores camera position, zoom, z-order, and +retained entity state with checksum and bounds validation. Agents may also +return a bounded `entities` array to showcase an answer directly with native +calendar, card, table, text, image, or browser entities. + +Agent submissions initially show only a small pulsing canvas icon. The +orchestrator chooses `action` presentation for simple one-off work, replacing +the icon with the result and creating no chat/session card, or `conversation` +presentation for work that benefits from retained discussion. The icon has an +animated hover label and can be dragged to choose the result location. Tables support row selection, and notifications behave as bottom-right toast cards: multiple toasts overlap compactly, fan into a spaced stack on hover,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/agent_response.c Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,237 @@ +#include "infinite_canvas/agent_response.h" + +#include <math.h> +#include <stdio.h> +#include <string.h> + +static boolean Canvas_Agent_Entity_Type( + const char *p_kind, + Canvas_Entity_Type *p_type) +{ + struct { + const char *p_name; + Canvas_Entity_Type type; + } types[] = { + {"text", CANVAS_ENTITY_TEXT}, + {"button", CANVAS_ENTITY_BUTTON}, + {"text_area", CANVAS_ENTITY_TEXT_AREA}, + {"accordion", CANVAS_ENTITY_ACCORDION}, + {"card", CANVAS_ENTITY_CARD}, + {"calendar", CANVAS_ENTITY_CALENDAR}, + {"table", CANVAS_ENTITY_TABLE}, + {"notification", CANVAS_ENTITY_NOTIFICATION}, + {"scroll_area", CANVAS_ENTITY_SCROLL_AREA}, + {"image", CANVAS_ENTITY_IMAGE}, + {"web_content", CANVAS_ENTITY_WEB_CONTENT}, + }; + for (size_t index = 0; index < sizeof(types) / sizeof(types[0]); index++) { + if (strcmp(p_kind, types[index].p_name) == 0) { + *p_type = types[index].type; + return TRUE; + } + } + return FALSE; +} + +static boolean Canvas_Agent_JSON_Number( + Dowa_JSON_Entry *p_object, + const char *p_key, + float *p_value, + boolean *p_present) +{ + Dowa_JSON_Value *p_json = Dowa_JSON_Get(p_object, p_key); + *p_present = p_json ? TRUE : FALSE; + if (!p_json) return TRUE; + if (p_json->type != DOWA_JSON_NUMBER || + !isfinite(p_json->num_val) || + fabs(p_json->num_val) > 10000000.0) { + return FALSE; + } + *p_value = (float)p_json->num_val; + return TRUE; +} + +static boolean Canvas_Agent_URL_Allowed(const char *p_value) +{ + return !p_value[0] || + strncmp(p_value, "https://", 8) == 0 || + strncmp(p_value, "http://", 7) == 0; +} + +boolean Canvas_Agent_Response_Parse( + const char *p_json, + Canvas_Agent_Response *p_response) +{ + if (!p_json || !p_response) return FALSE; + memset(p_response, 0, sizeof(*p_response)); + Dowa_Arena *p_arena = Dowa_Arena_Create(128 * 1024); + if (!p_arena) return FALSE; + Dowa_JSON_Value root = Dowa_JSON_Parse( + p_json, (int32)strlen(p_json), p_arena); + if (root.type != DOWA_JSON_OBJECT) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + Dowa_JSON_Entry *p_object = root.object_val; + char *p_action = Dowa_JSON_Get_String(p_object, "action"); + char *p_presentation = + Dowa_JSON_Get_String(p_object, "presentation"); + char *p_title = Dowa_JSON_Get_String(p_object, "title"); + char *p_text = Dowa_JSON_Get_String(p_object, "response"); + Dowa_JSON_Value *p_conversation = + Dowa_JSON_Get(p_object, "conversation_id"); + if (!p_action || + (strcmp(p_action, "create") != 0 && + strcmp(p_action, "append") != 0) || + !p_presentation || + (strcmp(p_presentation, "conversation") != 0 && + strcmp(p_presentation, "action") != 0) || + (strcmp(p_action, "append") == 0 && + strcmp(p_presentation, "conversation") != 0) || + !p_text || + !p_conversation || + p_conversation->type != DOWA_JSON_NUMBER || + floor(p_conversation->num_val) != p_conversation->num_val || + p_conversation->num_val < 0 || + p_conversation->num_val > 4294967295.0) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + snprintf( + p_response->action, + sizeof(p_response->action), + "%s", + p_action); + snprintf( + p_response->presentation, + sizeof(p_response->presentation), + "%s", + p_presentation); + snprintf( + p_response->title, + sizeof(p_response->title), + "%s", + p_title ? p_title : ""); + snprintf( + p_response->response, + sizeof(p_response->response), + "%s", + p_text); + p_response->conversation_id = (uint32)p_conversation->num_val; + + Dowa_JSON_Value *p_entities = Dowa_JSON_Get(p_object, "entities"); + if (p_entities) { + if (p_entities->type != DOWA_JSON_ARRAY || + Dowa_Array_Length(p_entities->array_val) > + CANVAS_AGENT_MAX_CREATED_ENTITIES) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + for (size_t index = 0; + index < Dowa_Array_Length(p_entities->array_val); + index++) { + Dowa_JSON_Value *p_value = &p_entities->array_val[index]; + if (p_value->type != DOWA_JSON_OBJECT) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + Dowa_JSON_Entry *p_entity_object = p_value->object_val; + char *p_kind = + Dowa_JSON_Get_String(p_entity_object, "kind"); + Canvas_Agent_Entity_Spec *p_spec = + &p_response->entities[p_response->entity_count]; + if (!p_kind || + !Canvas_Agent_Entity_Type(p_kind, &p_spec->type)) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + char *p_label = + Dowa_JSON_Get_String(p_entity_object, "label"); + char *p_entity_text = + Dowa_JSON_Get_String(p_entity_object, "text"); + snprintf( + p_spec->label, + sizeof(p_spec->label), + "%s", + p_label ? p_label : ""); + snprintf( + p_spec->text, + sizeof(p_spec->text), + "%s", + p_entity_text ? p_entity_text : ""); + if ((p_spec->type == CANVAS_ENTITY_IMAGE || + p_spec->type == CANVAS_ENTITY_WEB_CONTENT) && + !Canvas_Agent_URL_Allowed(p_spec->text)) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + float x = 0.0f; + float y = 0.0f; + boolean has_x = FALSE; + boolean has_y = FALSE; + if (!Canvas_Agent_JSON_Number( + p_entity_object, "x", &x, &has_x) || + !Canvas_Agent_JSON_Number( + p_entity_object, "y", &y, &has_y)) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + if (has_x != has_y) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + if ((has_x && (fabsf(x) > 1000000.0f || + fabsf(y) > 1000000.0f))) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + p_spec->has_position = has_x && has_y; + p_spec->position = (Vector2){x, y}; + float width = 0.0f; + float height = 0.0f; + boolean has_width = FALSE; + boolean has_height = FALSE; + if (!Canvas_Agent_JSON_Number( + p_entity_object, + "width", + &width, + &has_width) || + !Canvas_Agent_JSON_Number( + p_entity_object, + "height", + &height, + &has_height)) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + if (has_width != has_height || + (has_width && + (width < 32.0f || + height < 24.0f || + width > 5000.0f || + height > 5000.0f))) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + p_spec->has_size = has_width && has_height; + p_spec->size = (Vector2){width, height}; + Dowa_JSON_Value *p_entity_value = + Dowa_JSON_Get(p_entity_object, "value"); + if (p_entity_value) { + if (p_entity_value->type != DOWA_JSON_NUMBER || + floor(p_entity_value->num_val) != + p_entity_value->num_val || + p_entity_value->num_val < -2147483648.0 || + p_entity_value->num_val > 2147483647.0) { + Dowa_Arena_Free(p_arena); + return FALSE; + } + p_spec->value = (int32)p_entity_value->num_val; + p_spec->has_value = TRUE; + } + p_response->entity_count++; + } + } + Dowa_Arena_Free(p_arena); + return TRUE; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/agent_response.h Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,34 @@ +#ifndef INFINITE_CANVAS_AGENT_RESPONSE_H +#define INFINITE_CANVAS_AGENT_RESPONSE_H + +#include "infinite_canvas/canvas.h" + +#define CANVAS_AGENT_MAX_CREATED_ENTITIES 8 + +typedef struct { + Canvas_Entity_Type type; + char label[128]; + char text[CANVAS_ENTITY_TEXT_CAPACITY]; + Vector2 position; + Vector2 size; + int32 value; + boolean has_position; + boolean has_size; + boolean has_value; +} Canvas_Agent_Entity_Spec; + +typedef struct { + char action[32]; + char presentation[32]; + uint32 conversation_id; + char title[128]; + char response[CANVAS_ENTITY_TEXT_CAPACITY]; + Canvas_Agent_Entity_Spec entities[CANVAS_AGENT_MAX_CREATED_ENTITIES]; + uint32 entity_count; +} Canvas_Agent_Response; + +boolean Canvas_Agent_Response_Parse( + const char *p_json, + Canvas_Agent_Response *p_response); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/agent_response_test.c Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,45 @@ +#include "infinite_canvas/agent_response.h" + +#include <assert.h> +#include <string.h> + +int main(void) +{ + Canvas_Agent_Response response; + assert(Canvas_Agent_Response_Parse( + "{\"action\":\"create\",\"presentation\":\"action\"," + "\"conversation_id\":0," + "\"title\":\"Planning\",\"response\":\"Here is a calendar\"," + "\"entities\":[{\"kind\":\"calendar\",\"label\":\"August\"," + "\"x\":120,\"y\":80,\"width\":300,\"height\":260,\"value\":18}," + "{\"kind\":\"card\",\"label\":\"Launch\",\"text\":\"Ship it\"}]}", + &response)); + assert(strcmp(response.action, "create") == 0); + assert(strcmp(response.presentation, "action") == 0); + assert(response.entity_count == 2); + assert(response.entities[0].type == CANVAS_ENTITY_CALENDAR); + assert(response.entities[0].has_position); + assert(response.entities[0].has_size); + assert(response.entities[0].has_value); + assert(response.entities[0].value == 18); + assert(response.entities[1].type == CANVAS_ENTITY_CARD); + assert(!response.entities[1].has_value); + assert(strcmp(response.entities[1].text, "Ship it") == 0); + + assert(!Canvas_Agent_Response_Parse( + "{\"action\":\"create\",\"presentation\":\"action\"," + "\"conversation_id\":0," + "\"response\":\"bad\",\"entities\":[" + "{\"kind\":\"web_content\",\"text\":\"file:///etc/passwd\"}]}", + &response)); + assert(!Canvas_Agent_Response_Parse( + "{\"action\":\"create\",\"presentation\":\"action\"," + "\"conversation_id\":0," + "\"response\":\"bad\",\"entities\":[{\"kind\":\"unknown\"}]}", + &response)); + assert(!Canvas_Agent_Response_Parse( + "{\"action\":\"append\",\"presentation\":\"action\"," + "\"conversation_id\":4,\"response\":\"bad\"}", + &response)); + return 0; +}
--- a/infinite_canvas/agent_service_copilot.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/agent_service_copilot.c Tue Aug 18 22:18:15 2026 -0700 @@ -8,7 +8,7 @@ #include <string.h> #include <unistd.h> -#define CANVAS_AGENT_WARM_SESSION_COUNT 4 +#define CANVAS_AGENT_WARM_SESSION_COUNT 1 typedef struct { pthread_mutex_t mutex; @@ -145,9 +145,6 @@ } const char *warm_sessions[CANVAS_AGENT_WARM_SESSION_COUNT] = { "infinite-canvas-orchestrator", - "infinite-canvas-worker-1", - "infinite-canvas-worker-2", - "infinite-canvas-worker-3", }; for (uint32 index = 0; index < CANVAS_AGENT_WARM_SESSION_COUNT; @@ -165,7 +162,7 @@ INFERENCE_PROMPT_PROFILE_CANVAS_ORCHESTRATOR, 1, 1, - index == 0 ? TRUE : FALSE)) { + TRUE)) { Inference_Bridge_Destroy(p_copilot->p_bridge); pthread_mutex_destroy(&p_copilot->mutex); return FALSE;
--- a/infinite_canvas/canvas.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/canvas.c Tue Aug 18 22:18:15 2026 -0700 @@ -268,6 +268,22 @@ line_thickness, color); } + +} + +void Canvas_Draw_Lucide_Icon( + const char *p_name, + Vector2 origin, + float scale, + float line_thickness, + Color color) +{ + Canvas_Lucide_Draw_Icon( + Canvas_Lucide_Find_Icon(p_name), + origin, + scale, + line_thickness, + color); } static Rectangle Canvas_Web_Toolbar_Bounds( @@ -1633,24 +1649,32 @@ } } -static boolean Canvas_Web_Resize_Handle_Contains( +Rectangle Canvas_Entity_Resize_Handle_Bounds( + const Canvas_Entity *p_entity, + float zoom) +{ + float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom; + return (Rectangle){ + p_entity->position.x + p_entity->size.x - handle_size, + p_entity->position.y + p_entity->size.y - handle_size, + handle_size, + handle_size, + }; +} + +static boolean Canvas_Entity_Resize_Handle_Contains( const Canvas_Entity *p_entity, Vector2 point, float zoom) { if (p_entity->type != CANVAS_ENTITY_WEB_CONTENT && - p_entity->type != CANVAS_ENTITY_IMAGE) { + p_entity->type != CANVAS_ENTITY_IMAGE && + p_entity->type != CANVAS_ENTITY_TEXT_AREA) { return FALSE; } - float handle_size = CANVAS_WEB_RESIZE_HANDLE / zoom; return CheckCollisionPointRec( point, - (Rectangle){ - p_entity->position.x + p_entity->size.x - handle_size, - p_entity->position.y + p_entity->size.y - handle_size, - handle_size, - handle_size, - }) ? TRUE : FALSE; + Canvas_Entity_Resize_Handle_Bounds(p_entity, zoom)) ? TRUE : FALSE; } int32 Canvas_Scene_Pick( @@ -2603,11 +2627,17 @@ world_pointer, Canvas_Lucide_Search_Bounds(p_entity)) ? TRUE : FALSE; } + p_scene->resizing = Canvas_Entity_Resize_Handle_Contains( + p_entity, + world_pointer, + p_camera->zoom); p_scene->selecting_text = FALSE; if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) { - boolean inside_content = CheckCollisionPointRec( - world_pointer, - Canvas_Text_Area_Content_Bounds(p_entity)) ? TRUE : FALSE; + boolean inside_content = + !p_scene->resizing && + CheckCollisionPointRec( + world_pointer, + Canvas_Text_Area_Content_Bounds(p_entity)); p_entity->active = inside_content; if (inside_content) { int32 cursor = Canvas_Text_Cursor_At_Point( @@ -2623,10 +2653,6 @@ } p_scene->pressed_index = p_scene->selected_index; p_scene->dragging = FALSE; - p_scene->resizing = Canvas_Web_Resize_Handle_Contains( - p_entity, - world_pointer, - p_camera->zoom); p_scene->pointer_start = world_pointer; p_scene->entity_start = p_entity->position; p_scene->entity_size_start = p_entity->size; @@ -2663,10 +2689,25 @@ if (p_entity->type == CANVAS_ENTITY_NOTIFICATION) { p_scene->dragging = FALSE; } else if (p_scene->resizing) { + float min_width = + p_entity->type == CANVAS_ENTITY_TEXT_AREA + ? CANVAS_TEXT_AREA_MIN_WIDTH + : CANVAS_WEB_MIN_WIDTH; + float min_height = + p_entity->type == CANVAS_ENTITY_TEXT_AREA + ? CANVAS_TEXT_AREA_MIN_HEIGHT + : CANVAS_WEB_MIN_HEIGHT; p_entity->size = (Vector2){ - fmaxf(CANVAS_WEB_MIN_WIDTH, p_scene->entity_size_start.x + delta.x), - fmaxf(CANVAS_WEB_MIN_HEIGHT, p_scene->entity_size_start.y + delta.y), + fmaxf( + min_width, + p_scene->entity_size_start.x + delta.x), + fmaxf( + min_height, + p_scene->entity_size_start.y + delta.y), }; + if (p_entity->type == CANVAS_ENTITY_TEXT_AREA) { + Canvas_Text_Ensure_Cursor_Visible(p_entity, font); + } } else { p_entity->position = (Vector2){ p_scene->entity_start.x + delta.x, @@ -3059,32 +3100,31 @@ 10, line, selection); - DrawRectangleRec( - (Rectangle){ - p_entity->position.x + p_entity->size.x - - CANVAS_WEB_RESIZE_HANDLE / zoom, - p_entity->position.y + p_entity->size.y - - CANVAS_WEB_RESIZE_HANDLE / zoom, - CANVAS_WEB_RESIZE_HANDLE / zoom, - CANVAS_WEB_RESIZE_HANDLE / zoom, - }, - Canvas_Color_Fade(selection, 0.18f)); - DrawLineEx( - (Vector2){ - p_entity->position.x + p_entity->size.x - 11.0f / zoom, - p_entity->position.y + p_entity->size.y - 4.0f / zoom, - }, - (Vector2){ - p_entity->position.x + p_entity->size.x - 4.0f / zoom, - p_entity->position.y + p_entity->size.y - 11.0f / zoom, - }, - 1.5f / zoom, - selection); break; default: break; } + if (p_entity->type == CANVAS_ENTITY_TEXT_AREA || + p_entity->type == CANVAS_ENTITY_IMAGE || + p_entity->type == CANVAS_ENTITY_WEB_CONTENT) { + Rectangle handle = + Canvas_Entity_Resize_Handle_Bounds(p_entity, zoom); + DrawRectangleRec( + handle, + Canvas_Color_Fade(selection, 0.18f)); + DrawLineEx( + (Vector2){ + handle.x + handle.width - 11.0f / zoom, + handle.y + handle.height - 4.0f / zoom, + }, + (Vector2){ + handle.x + handle.width - 4.0f / zoom, + handle.y + handle.height - 11.0f / zoom, + }, + 1.5f / zoom, + selection); + } } static void Canvas_Draw_Text_Area_Content(
--- a/infinite_canvas/canvas.h Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/canvas.h Tue Aug 18 22:18:15 2026 -0700 @@ -11,6 +11,8 @@ #define CANVAS_WEB_RESIZE_HANDLE 18.0f #define CANVAS_WEB_MIN_WIDTH 160.0f #define CANVAS_WEB_MIN_HEIGHT 120.0f +#define CANVAS_TEXT_AREA_MIN_WIDTH 180.0f +#define CANVAS_TEXT_AREA_MIN_HEIGHT 96.0f #define CANVAS_CONTEXT_MAX_LENGTH (64 * 1024) #define CANVAS_ENTITY_TEXT_CAPACITY 2048 @@ -129,7 +131,16 @@ float padding_right, float padding_bottom); float Canvas_Grid_Spacing(float zoom); +Rectangle Canvas_Entity_Resize_Handle_Bounds( + const Canvas_Entity *p_entity, + float zoom); int32 Canvas_Lucide_Count_Matches(const char *p_query); +void Canvas_Draw_Lucide_Icon( + const char *p_name, + Vector2 origin, + float scale, + float line_thickness, + Color color); Canvas_Web_Chrome_Action Canvas_Web_Chrome_Update_Input( const Canvas_Camera *p_camera, Canvas_Scene *p_scene,
--- a/infinite_canvas/canvas_test.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/canvas_test.c Tue Aug 18 22:18:15 2026 -0700 @@ -100,6 +100,22 @@ assert(Near(before.y, after.y)); } +static void Test_Text_Area_Resize_Handle(void) +{ + Canvas_Entity entity = { + .type = CANVAS_ENTITY_TEXT_AREA, + .position = {100.0f, 200.0f}, + .size = {280.0f, 148.0f}, + }; + Rectangle handle = Canvas_Entity_Resize_Handle_Bounds(&entity, 2.0f); + assert(Near(handle.x, 371.0f)); + assert(Near(handle.y, 339.0f)); + assert(Near(handle.width, 9.0f)); + assert(Near(handle.height, 9.0f)); + assert(CANVAS_TEXT_AREA_MIN_WIDTH < entity.size.x); + assert(CANVAS_TEXT_AREA_MIN_HEIGHT < entity.size.y); +} + static void Test_Viewport_Bounds(void) { Canvas_Camera camera; @@ -658,6 +674,7 @@ Test_Theme_Resolution(); Test_Camera_Round_Trip(); Test_Anchored_Zoom(); + Test_Text_Area_Resize_Handle(); Test_Viewport_Bounds(); Test_Grid_Spacing(); Test_Entity_Picking();
--- a/infinite_canvas/dev_ui.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/dev_ui.c Tue Aug 18 22:18:15 2026 -0700 @@ -1,11 +1,12 @@ #include "infinite_canvas/dev_ui.h" #include <math.h> +#include <stdio.h> #define RAYGUI_IMPLEMENTATION #include "third_party/raylib/include/raygui.h" -static const Rectangle DEV_PANEL = {18.0f, 18.0f, 264.0f, 520.0f}; +static const Rectangle DEV_PANEL = {18.0f, 18.0f, 264.0f, 650.0f}; static const Rectangle DEV_PANEL_COLLAPSED = {18.0f, 18.0f, 44.0f, 44.0f}; static Rectangle Canvas_Dev_UI_Context_Bounds(void) @@ -211,14 +212,33 @@ .dropdown_open = FALSE, .context_open = FALSE, }; + snprintf( + p_ui->snapshot_name, + sizeof(p_ui->snapshot_name), + "workspace"); + snprintf( + p_ui->snapshot_status, + sizeof(p_ui->snapshot_status), + "Native snapshots use a versioned .zmap file"); Canvas_Dev_UI_Set_Style(font, p_theme); } +void Canvas_Dev_UI_Set_Snapshot_Status( + Canvas_Dev_UI *p_ui, + const char *p_status) +{ + snprintf( + p_ui->snapshot_status, + sizeof(p_ui->snapshot_status), + "%s", + p_status ? p_status : ""); +} + boolean Canvas_Dev_UI_Contains_Pointer(const Canvas_Dev_UI *p_ui) { // The expanded dropdown is modal: it draws outside DEV_PANEL and also // consumes outside clicks to close itself. - if (p_ui->dropdown_open) return TRUE; + if (p_ui->dropdown_open || p_ui->snapshot_name_editing) return TRUE; Vector2 pointer = GetMousePosition(); if (CheckCollisionPointRec( @@ -233,13 +253,19 @@ return FALSE; } -void Canvas_Dev_UI_Draw( +boolean Canvas_Dev_UI_Is_Editing(const Canvas_Dev_UI *p_ui) +{ + return p_ui->dropdown_open || p_ui->snapshot_name_editing; +} + +Canvas_Dev_UI_Action Canvas_Dev_UI_Draw( Canvas_Dev_UI *p_ui, Canvas_Camera *p_camera, Canvas_Scene *p_scene, Font font, Canvas_Theme *p_theme) { + Canvas_Dev_UI_Action action = CANVAS_DEV_UI_ACTION_NONE; if (p_ui->collapsed) { Canvas_Theme_Draw_Shadow( p_theme, @@ -266,7 +292,7 @@ 10)) { p_ui->collapsed = FALSE; } - return; + return action; } Canvas_Context_Snapshot context = Canvas_Scene_Build_Visible_Context( @@ -299,7 +325,7 @@ p_ui->collapsed = TRUE; p_ui->dropdown_open = FALSE; p_ui->context_open = FALSE; - return; + return action; } if (p_ui->dropdown_open) GuiLock(); @@ -376,19 +402,57 @@ Canvas_Scene_Show_Parking_Lot(p_scene, p_camera); } - DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 438.0f}, 12.0f, 0.0f, p_theme->text_muted); - DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 438.0f}, 12.0f, 0.0f, p_theme->text_muted); + DrawTextEx( + font, + "Named canvas snapshot", + (Vector2){34.0f, 424.0f}, + 12.0f, + 0.0f, + p_theme->text_muted); + if (GuiTextBox( + (Rectangle){34.0f, 444.0f, 232.0f, 34.0f}, + p_ui->snapshot_name, + (int32)sizeof(p_ui->snapshot_name), + p_ui->snapshot_name_editing)) { + p_ui->snapshot_name_editing = !p_ui->snapshot_name_editing; + } + if (GuiButtonRounded( + (Rectangle){34.0f, 486.0f, 112.0f, 34.0f}, + "Save", + 0.32f, + 10)) { + action = CANVAS_DEV_UI_ACTION_SAVE_SNAPSHOT; + p_ui->snapshot_name_editing = FALSE; + } + if (GuiButtonRounded( + (Rectangle){154.0f, 486.0f, 112.0f, 34.0f}, + "Load", + 0.32f, + 10)) { + action = CANVAS_DEV_UI_ACTION_LOAD_SNAPSHOT; + p_ui->snapshot_name_editing = FALSE; + } + DrawTextEx( + font, + p_ui->snapshot_status, + (Vector2){34.0f, 530.0f}, + 11.0f, + 0.0f, + p_theme->text_muted); + + DrawTextEx(font, TextFormat("%d FPS", GetFPS()), (Vector2){34.0f, 570.0f}, 12.0f, 0.0f, p_theme->text_muted); + DrawTextEx(font, TextFormat("%.2fx zoom", p_camera->zoom), (Vector2){104.0f, 570.0f}, 12.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, TextFormat("%d objects", (int)Dowa_Array_Length(p_scene->p_entities)), - (Vector2){196.0f, 438.0f}, + (Vector2){196.0f, 570.0f}, 12.0f, 0.0f, p_theme->text_muted); DrawTextEx( font, "Drag objects directly on the canvas", - (Vector2){34.0f, 484.0f}, + (Vector2){34.0f, 616.0f}, 12.0f, 0.0f, p_theme->text_muted); @@ -413,4 +477,5 @@ 10)) { p_ui->dropdown_open = !p_ui->dropdown_open; } + return action; }
--- a/infinite_canvas/dev_ui.h Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/dev_ui.h Tue Aug 18 22:18:15 2026 -0700 @@ -2,6 +2,7 @@ #define INFINITE_CANVAS_DEV_UI_H #include "infinite_canvas/canvas.h" +#include "infinite_canvas/scene_store.h" typedef struct { int32 selected_type; @@ -11,18 +12,31 @@ float context_scroll; float context_content_height; char context_buffer[CANVAS_CONTEXT_MAX_LENGTH]; + char snapshot_name[CANVAS_SCENE_STORE_NAME_CAPACITY]; + char snapshot_status[96]; + boolean snapshot_name_editing; } Canvas_Dev_UI; +typedef enum { + CANVAS_DEV_UI_ACTION_NONE = 0, + CANVAS_DEV_UI_ACTION_SAVE_SNAPSHOT, + CANVAS_DEV_UI_ACTION_LOAD_SNAPSHOT, +} Canvas_Dev_UI_Action; + void Canvas_Dev_UI_Init( Canvas_Dev_UI *p_ui, Font font, const Canvas_Theme *p_theme); boolean Canvas_Dev_UI_Contains_Pointer(const Canvas_Dev_UI *p_ui); -void Canvas_Dev_UI_Draw( +boolean Canvas_Dev_UI_Is_Editing(const Canvas_Dev_UI *p_ui); +Canvas_Dev_UI_Action Canvas_Dev_UI_Draw( Canvas_Dev_UI *p_ui, Canvas_Camera *p_camera, Canvas_Scene *p_scene, Font font, Canvas_Theme *p_theme); +void Canvas_Dev_UI_Set_Snapshot_Status( + Canvas_Dev_UI *p_ui, + const char *p_status); #endif
--- a/infinite_canvas/docs/agent-sessions.md Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/docs/agent-sessions.md Tue Aug 18 22:18:15 2026 -0700 @@ -24,8 +24,17 @@ `Canvas_Scene_Build_Visible_Context()` through the shared asynchronous `Inference_Bridge`. 6. The Bazel-managed Copilot SDK sidecar uses the `canvas_orchestrator` profile - and returns JSON choosing `create` or `append` and a conversation entity ID. - The canvas materializes that decision as a retained rich conversation card. + and returns JSON choosing `action` or `conversation` presentation, + `create` or `append` routing, a conversation entity ID, and optional typed + showcase entities. The canvas materializes only the requested surface. + +All submissions begin as one small pulsing Raylib/Lucide indicator anchored to +the board. Hovering reveals its working label, and dragging it changes the +world position where an eventual result or conversation is materialized. A +one-off action replaces it with the resulting entity or compact notification +and does not create another agent/session. Complex work creates a retained +conversation card after orchestration completes; continuations append to an +already-visible conversation. The scratchpad is excluded from serialized camera context because the submitted thought is already sent separately. Only conversation entities currently in @@ -41,12 +50,10 @@ gateway for authenticated GitHub Copilot inference. Only one orchestration request is admitted at a time in this prototype. -`agent_service_copilot.c` warms that orchestration session plus three reserved -worker sessions during initialization. They share the same -`canvas_orchestrator` profile and move Copilot SDK client/session setup ahead of -the first user turn. The persistent orchestrator is resumed when available; -reserved workers are created fresh because they have no conversation history -to recover, avoiding expected `session.resume` errors during startup. +`agent_service_copilot.c` warms only the persistent orchestration session +during initialization. This moves Copilot SDK client/session setup ahead of the +first user turn without creating unused worker sessions. The orchestrator is +resumed when available. ## Conversation entities @@ -78,6 +85,10 @@ - One persistent Dictation scratchpad is reused and cleared after submission. Submitted turns are retained in conversation entities after orchestration. - One Copilot request runs at a time. +- Simple `action` presentation stays in the primary orchestrator and does not + create a conversation card, worker, or sub-session. +- Agent-created entities pass through a strict type allowlist and count bound. + Browser and image entities accept only absolute HTTP(S) URLs. - Conversation transcript storage is currently bounded by `CANVAS_ENTITY_TEXT_CAPACITY`. - Linux uses the Copilot SDK bridge. Other platforms currently select an
--- a/infinite_canvas/docs/entities-and-components.md Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/docs/entities-and-components.md Tue Aug 18 22:18:15 2026 -0700 @@ -60,9 +60,10 @@ - hovered/pressed scene entity; - camera. -Open developer dropdowns are modal because Raygui draws their item list beyond -the panel rectangle and consumes outside clicks to close. While one is open, -neither native web surfaces nor canvas entities receive pointer input behind it. +Open developer dropdowns and the snapshot-name editor are modal because Raygui +consumes keyboard or outside-click input while editing. While either is open, +native web surfaces, canvas entities, dictation hotkeys, and camera movement do +not receive that input. Text editing blocks WASD/arrow camera movement. Scrollable entities consume an unmodified wheel. Ctrl/Cmd + wheel remains available to the camera for anchored @@ -72,4 +73,26 @@ entity. Their visual-line layout is shared by drawing, hit testing, vertical cursor movement, and caret visibility, so wrapped text and mouse selection use the same geometry. Ctrl on Windows/Linux and Cmd on macOS drive select/copy/ -cut/paste shortcuts; Ctrl or Option performs word navigation. +cut/paste shortcuts; Ctrl or Option performs word navigation. A selected text +area exposes a bottom-right resize handle. Dragging it changes both dimensions, +reflows wrapped lines immediately, and keeps the caret within the scrollable +viewport. + +## Snapshot boundary + +Developer Controls saves named native snapshots under the user state directory +as versioned `.zmap` files. The encoder writes explicit little-endian fields +rather than copying `Canvas_Entity` memory, then uses a payload checksum, +bounded entity count, finite geometry validation, and an atomic temporary-file +rename. Loading is transactional: the live scene is not cleared until the +entire snapshot validates. + +The current snapshot preserves numeric canvas entity IDs because conversation +routing uses them. Those IDs are canvas-local handles, not final +cross-workspace identities. Domain data should eventually normalize into +stable keys such as `conversation_id`, `asset_id`, and `component_id`; an +entity snapshot should retain only the typed key plus its visual state. + +Transient pointers, CEF surfaces, Copilot SDK sessions, hover state, active +agent work, and removal animations are never serialized. Browser and image +surfaces reconstruct from their retained HTTP(S) source after load.
--- a/infinite_canvas/main.c Tue Aug 18 19:14:53 2026 -0700 +++ b/infinite_canvas/main.c Tue Aug 18 22:18:15 2026 -0700 @@ -1,5 +1,7 @@ +#include "infinite_canvas/agent_response.h" #include "infinite_canvas/agent_service.h" #include "infinite_canvas/canvas.h" +#include "infinite_canvas/scene_store.h" #include "infinite_canvas/web_surface.h" #if defined(INFINITE_CANVAS_DEV_UI) @@ -27,9 +29,14 @@ const char *p_screenshot_path; uint32 frame_count; uint32 screenshot_frame; - uint32 pending_conversation_id; + Vector2 pending_indicator_position; + Vector2 pending_indicator_drag_offset; uint32 dictation_entity_id; boolean agent_initialized; + boolean pending_indicator_visible; + boolean pending_indicator_hovered; + boolean pending_indicator_dragging; + float pending_indicator_hover_amount; boolean dictation_overlay_visible; boolean dictation_listening; boolean dictation_requested_active; @@ -46,48 +53,6 @@ static Canvas_App *g_app = NULL; -static const char *Canvas_App_JSON_String( - const char *p_json, - const char *p_key, - char *p_output, - size_t output_capacity) -{ - char needle[128]; - snprintf(needle, sizeof(needle), "\"%s\"", p_key); - const char *p_cursor = strstr(p_json, needle); - if (!p_cursor) return NULL; - p_cursor = strchr(p_cursor + strlen(needle), ':'); - if (!p_cursor) return NULL; - while (*++p_cursor == ' ') {} - if (*p_cursor != '"') return NULL; - p_cursor++; - size_t output = 0; - while (*p_cursor && *p_cursor != '"' && output + 1 < output_capacity) { - if (*p_cursor == '\\' && p_cursor[1]) { - p_cursor++; - if (*p_cursor == 'n') p_output[output++] = '\n'; - else if (*p_cursor == 't') p_output[output++] = '\t'; - else if (*p_cursor != 'r') p_output[output++] = *p_cursor; - } else { - p_output[output++] = *p_cursor; - } - p_cursor++; - } - p_output[output] = '\0'; - return p_output; -} - -static uint32 Canvas_App_JSON_Uint(const char *p_json, const char *p_key) -{ - char needle[128]; - snprintf(needle, sizeof(needle), "\"%s\"", p_key); - const char *p_cursor = strstr(p_json, needle); - if (!p_cursor) return 0; - p_cursor = strchr(p_cursor + strlen(needle), ':'); - if (!p_cursor) return 0; - return (uint32)strtoul(p_cursor + 1, NULL, 10); -} - static Canvas_Entity *Canvas_App_Find_Entity(uint32 entity_id) { for (size_t index = 0; @@ -100,38 +65,6 @@ return NULL; } -static void Canvas_App_Set_Conversation_Text( - Canvas_Entity *p_entity, - const char *p_prompt, - const char *p_speaker, - const char *p_response) -{ - size_t length = 0; - int32 written = snprintf( - p_entity->text, - sizeof(p_entity->text), - "You\n"); - if (written > 0) length = (size_t)written; - written = snprintf( - p_entity->text + length, - sizeof(p_entity->text) - length, - "%.*s\n\n%s\n", - 900, - p_prompt, - p_speaker); - if (written > 0) length += (size_t)written; - if (length >= sizeof(p_entity->text)) { - length = sizeof(p_entity->text) - 1; - } - snprintf( - p_entity->text + length, - sizeof(p_entity->text) - length, - "%.*s", - 900, - p_response); - Canvas_Conversation_Scroll_To_End(p_entity, g_app->font); -} - static boolean Canvas_App_Submit_Prompt(const char *p_prompt) { if (!p_prompt || !p_prompt[0]) return FALSE; @@ -151,85 +84,140 @@ sizeof(g_app->pending_prompt), "%s", p_prompt); - Vector2 position = Canvas_Camera_Screen_To_World( + g_app->pending_indicator_position = Canvas_Camera_Screen_To_World( &g_app->camera, (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, + (float)g_app->camera.viewport_width * 0.5f + 240.0f, (float)g_app->camera.viewport_height * 0.5f - 150.0f, }); - g_app->pending_conversation_id = Canvas_Scene_Add_Conversation( - &g_app->scene, - "Routing thought...", - p_prompt, - "Copilot is deciding whether to create or extend a session.", - position); - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (p_pending) p_pending->agent_working = TRUE; + g_app->pending_indicator_visible = TRUE; + g_app->pending_indicator_hovered = FALSE; + g_app->pending_indicator_dragging = FALSE; + g_app->pending_indicator_hover_amount = 0.0f; return TRUE; } +static uint32 Canvas_App_Materialize_Agent_Entities( + Canvas_Agent_Response *p_response) +{ + uint32 created = 0; + for (uint32 index = 0; index < p_response->entity_count; index++) { + Canvas_Agent_Entity_Spec *p_spec = &p_response->entities[index]; + Vector2 position = p_spec->has_position + ? p_spec->position + : (Vector2){ + g_app->pending_indicator_position.x + + (float)(index % 2) * 280.0f, + g_app->pending_indicator_position.y + + (float)(index / 2) * 240.0f, + }; + if (!Canvas_Scene_Add(&g_app->scene, p_spec->type, position)) { + break; + } + Canvas_Entity *p_entity = &g_app->scene.p_entities[ + Dowa_Array_Length(g_app->scene.p_entities) - 1]; + if (p_spec->label[0]) { + snprintf( + p_entity->label, + sizeof(p_entity->label), + "%s", + p_spec->label); + } + if (p_spec->text[0]) { + snprintf( + p_entity->text, + sizeof(p_entity->text), + "%s", + p_spec->text); + p_entity->text_cursor = (int32)strlen(p_entity->text); + p_entity->text_selection_anchor = p_entity->text_cursor; + } + if (p_spec->has_size) p_entity->size = p_spec->size; + if (p_spec->has_value) p_entity->value = p_spec->value; + created++; + } + return created; +} + +static void Canvas_App_Show_Agent_Notification( + const char *p_title, + const char *p_text) +{ + if (!Canvas_Scene_Add( + &g_app->scene, + CANVAS_ENTITY_NOTIFICATION, + g_app->pending_indicator_position)) { + return; + } + Canvas_Entity *p_entity = &g_app->scene.p_entities[ + Dowa_Array_Length(g_app->scene.p_entities) - 1]; + if (p_title && p_title[0]) { + snprintf( + p_entity->label, + sizeof(p_entity->label), + "%s", + p_title); + } + snprintf( + p_entity->text, + sizeof(p_entity->text), + "%s", + p_text ? p_text : ""); +} + static void Canvas_App_Apply_Agent_Response(const char *p_json) { - char action[32] = {0}; - char title[128] = {0}; - char response[CANVAS_ENTITY_TEXT_CAPACITY] = {0}; - Canvas_App_JSON_String(p_json, "action", action, sizeof(action)); - Canvas_App_JSON_String(p_json, "title", title, sizeof(title)); - if (!Canvas_App_JSON_String( - p_json, - "response", - response, - sizeof(response))) { - snprintf(response, sizeof(response), "%s", p_json); + Canvas_Agent_Response parsed; + boolean valid = Canvas_Agent_Response_Parse(p_json, &parsed); + if (!valid) { + memset(&parsed, 0, sizeof(parsed)); + snprintf(parsed.action, sizeof(parsed.action), "create"); + snprintf( + parsed.presentation, + sizeof(parsed.presentation), + "conversation"); + snprintf( + parsed.response, + sizeof(parsed.response), + "%s", + p_json); } - uint32 conversation_id = - Canvas_App_JSON_Uint(p_json, "conversation_id"); - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (strcmp(action, "append") == 0 && - conversation_id != 0 && + uint32 created_entities = + Canvas_App_Materialize_Agent_Entities(&parsed); + if (strcmp(parsed.action, "append") == 0 && + parsed.conversation_id != 0 && Canvas_Scene_Append_Conversation( &g_app->scene, - conversation_id, + parsed.conversation_id, g_app->pending_prompt, - response)) { + parsed.response)) { Canvas_Entity *p_conversation = - Canvas_App_Find_Entity(conversation_id); + Canvas_App_Find_Entity(parsed.conversation_id); Canvas_Conversation_Scroll_To_End( p_conversation, g_app->font); - if (p_pending) p_pending->removing = TRUE; - } else if (p_pending) { - snprintf( - p_pending->label, - sizeof(p_pending->label), - "%s", - title[0] ? title : "Copilot session"); - Canvas_App_Set_Conversation_Text( - p_pending, - g_app->pending_prompt, - "Copilot", - response); - p_pending->agent_working = FALSE; + } else if (strcmp(parsed.presentation, "action") == 0) { + if (created_entities == 0) { + Canvas_App_Show_Agent_Notification( + parsed.title[0] ? parsed.title : "Copilot", + parsed.response); + } } else { - Vector2 position = Canvas_Camera_Screen_To_World( - &g_app->camera, - (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, - (float)g_app->camera.viewport_height * 0.5f - 150.0f, - }); + Vector2 position = { + g_app->pending_indicator_position.x - 210.0f, + g_app->pending_indicator_position.y - 150.0f, + }; uint32 created_id = Canvas_Scene_Add_Conversation( &g_app->scene, - title[0] ? title : "Copilot session", + parsed.title[0] ? parsed.title : "Copilot session", g_app->pending_prompt, - response, + parsed.response, position); Canvas_Conversation_Scroll_To_End( Canvas_App_Find_Entity(created_id), g_app->font); } - g_app->pending_conversation_id = 0; + g_app->pending_indicator_visible = FALSE; g_app->pending_prompt[0] = '\0'; } @@ -421,6 +409,194 @@ g_app->theme.text_muted); } +static void Canvas_App_Draw_Agent_Indicator(void) +{ + if (!g_app->pending_indicator_visible) return; + Vector2 screen = Canvas_Camera_World_To_Screen( + &g_app->camera, + g_app->pending_indicator_position); + float pulse = 0.5f + 0.5f * sinf((float)GetTime() * 5.0f); + float hover = g_app->pending_indicator_hover_amount; + float radius = 22.0f + hover * 2.0f; + Rectangle bounds = { + screen.x - radius, + screen.y - radius, + radius * 2.0f, + radius * 2.0f, + }; + Canvas_Theme_Draw_Shadow( + &g_app->theme, + bounds, + 0.5f, + 16, + 1.0f, + 1.0f); + DrawCircleV(screen, radius, g_app->theme.surface); + DrawCircleLinesV( + screen, + 20.0f + pulse * 2.0f, + Fade(g_app->theme.accent, 0.45f + pulse * 0.35f)); + Canvas_Draw_Lucide_Icon( + "sparkles", + (Vector2){screen.x - 9.0f, screen.y - 9.0f}, + 0.75f, + 1.8f, + g_app->theme.accent); + if (hover > 0.01f) { + Rectangle label = { + screen.x + 28.0f - (1.0f - hover) * 8.0f, + screen.y - 17.0f, + 118.0f, + 34.0f, + }; + DrawRectangleRounded( + label, + 0.35f, + 10, + Fade(g_app->theme.surface, hover)); + DrawRectangleRoundedLinesEx( + label, + 0.35f, + 10, + 1.0f, + Fade(g_app->theme.border, hover)); + DrawTextEx( + g_app->font, + g_app->pending_indicator_dragging + ? "Move result" + : "Copilot working", + (Vector2){label.x + 12.0f, label.y + 10.0f}, + 13.0f, + 0.0f, + Fade(g_app->theme.text, hover)); + } +} + +static boolean Canvas_App_Update_Agent_Indicator_Input(boolean blocked) +{ + if (!g_app->pending_indicator_visible) { + g_app->pending_indicator_hovered = FALSE; + g_app->pending_indicator_dragging = FALSE; + g_app->pending_indicator_hover_amount = 0.0f; + return FALSE; + } + Vector2 pointer = GetMousePosition(); + Vector2 screen = Canvas_Camera_World_To_Screen( + &g_app->camera, + g_app->pending_indicator_position); + if (g_app->pending_indicator_dragging) { + Vector2 target_screen = { + pointer.x - g_app->pending_indicator_drag_offset.x, + pointer.y - g_app->pending_indicator_drag_offset.y, + }; + g_app->pending_indicator_position = + Canvas_Camera_Screen_To_World( + &g_app->camera, + target_screen); + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { + g_app->pending_indicator_dragging = FALSE; + } + g_app->pending_indicator_hovered = TRUE; + } else { + g_app->pending_indicator_hovered = + !blocked && + CheckCollisionPointCircle(pointer, screen, 26.0f); + if (g_app->pending_indicator_hovered && + IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + g_app->pending_indicator_dragging = TRUE; + g_app->pending_indicator_drag_offset = (Vector2){ + pointer.x - screen.x, + pointer.y - screen.y, + }; + } + } + float target = g_app->pending_indicator_hovered ? 1.0f : 0.0f; + float step = GetFrameTime() * 10.0f; + if (g_app->pending_indicator_hover_amount < target) { + g_app->pending_indicator_hover_amount = fminf( + target, + g_app->pending_indicator_hover_amount + step); + } else { + g_app->pending_indicator_hover_amount = fmaxf( + target, + g_app->pending_indicator_hover_amount - step); + } + return g_app->pending_indicator_hovered || + g_app->pending_indicator_dragging; +} + +#if defined(INFINITE_CANVAS_DEV_UI) +static void Canvas_App_Handle_Dev_Action(Canvas_Dev_UI_Action action) +{ + if (action == CANVAS_DEV_UI_ACTION_NONE) return; + char path[CANVAS_SCENE_STORE_PATH_CAPACITY]; + if (!Canvas_Scene_Store_Path( + g_app->dev_ui.snapshot_name, + path, + sizeof(path))) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Use only letters, numbers, - or _"); + return; + } + if (action == CANVAS_DEV_UI_ACTION_SAVE_SNAPSHOT) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + Canvas_Scene_Store_Save( + path, + &g_app->scene, + &g_app->camera) + ? "Snapshot saved" + : "Snapshot save failed"); + return; + } + if (g_app->agent_initialized && + g_app->agent_service.status == CANVAS_AGENT_WORKING) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Wait for the active agent turn before loading"); + return; + } + if (!Canvas_Scene_Store_Load( + path, + &g_app->scene, + &g_app->camera)) { + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Snapshot missing, incompatible, or corrupt"); + return; + } + g_app->pending_indicator_visible = FALSE; + g_app->pending_prompt[0] = '\0'; + g_app->dictation_entity_id = 0; + g_app->dictation_transcript[0] = '\0'; + g_app->dictation_partial[0] = '\0'; + for (size_t index = 0; + index < Dowa_Array_Length(g_app->scene.p_entities); + index++) { + Canvas_Entity *p_entity = &g_app->scene.p_entities[index]; + if (p_entity->type == CANVAS_ENTITY_TEXT_AREA && + strcmp(p_entity->label, "Dictation") == 0) { + g_app->dictation_entity_id = p_entity->id; + snprintf( + p_entity->text_muted + ? g_app->dictation_partial + : g_app->dictation_transcript, + CANVAS_ENTITY_TEXT_CAPACITY, + "%s", + p_entity->text); + break; + } + } + g_app->dictation_overlay_visible = + g_app->dictation_transcript[0] || + g_app->dictation_partial[0]; + Canvas_Dev_UI_Set_Snapshot_Status( + &g_app->dev_ui, + "Snapshot loaded"); +} +#endif + static void Canvas_App_Draw_Frame(void) { int32 width = GetScreenWidth(); @@ -428,13 +604,19 @@ Canvas_Camera_Set_Viewport(&g_app->camera, width, height); boolean block_pointer_input = FALSE; + boolean dev_ui_editing = FALSE; #if defined(INFINITE_CANVAS_DEV_UI) block_pointer_input = Canvas_Dev_UI_Contains_Pointer(&g_app->dev_ui); + dev_ui_editing = Canvas_Dev_UI_Is_Editing(&g_app->dev_ui); #endif + block_pointer_input = + Canvas_App_Update_Agent_Indicator_Input(block_pointer_input) || + block_pointer_input; Canvas_Web_Surface_Set_Dark_Mode( &g_app->web_surface, g_app->theme.resolved_mode == CANVAS_THEME_DARK ? TRUE : FALSE); boolean dictation_hotkey = + !dev_ui_editing && !Canvas_Scene_Is_Text_Editing(&g_app->scene) && !Canvas_Web_Chrome_Is_Editing(&g_app->scene) && IsKeyPressed(KEY_M); @@ -496,6 +678,7 @@ boolean keyboard_focus = Canvas_Scene_Is_Text_Editing(&g_app->scene) || Canvas_Web_Chrome_Is_Editing(&g_app->scene) || + dev_ui_editing || Canvas_Web_Surface_Is_Focused(&g_app->web_surface); if (!keyboard_focus && IsKeyPressed(KEY_P)) { Canvas_Scene_Toggle_Selected_Pin(&g_app->scene, &g_app->camera); @@ -614,31 +797,12 @@ if (agent_status == CANVAS_AGENT_READY) { Canvas_App_Apply_Agent_Response(agent_response); } else if (agent_status == CANVAS_AGENT_ERROR) { - Canvas_Entity *p_pending = - Canvas_App_Find_Entity(g_app->pending_conversation_id); - if (p_pending) { - snprintf(p_pending->label, sizeof(p_pending->label), "Copilot unavailable"); - Canvas_App_Set_Conversation_Text( - p_pending, - g_app->pending_prompt, - "System", + if (g_app->pending_prompt[0]) { + Canvas_App_Show_Agent_Notification( + "Copilot unavailable", agent_error); - p_pending->agent_working = FALSE; - } else if (g_app->pending_prompt[0]) { - Vector2 position = Canvas_Camera_Screen_To_World( - &g_app->camera, - (Vector2){ - (float)g_app->camera.viewport_width * 0.5f - 210.0f, - (float)g_app->camera.viewport_height * 0.5f - 150.0f, - }); - Canvas_Scene_Add_Conversation( - &g_app->scene, - "Copilot unavailable", - g_app->pending_prompt, - agent_error, - position); } - g_app->pending_conversation_id = 0; + g_app->pending_indicator_visible = FALSE; g_app->pending_prompt[0] = '\0'; } @@ -687,8 +851,9 @@ &g_app->scene, g_app->font, &g_app->theme); + Canvas_App_Draw_Agent_Indicator(); #if defined(INFINITE_CANVAS_DEV_UI) - Canvas_Dev_UI_Draw( + Canvas_Dev_UI_Action dev_action = Canvas_Dev_UI_Draw( &g_app->dev_ui, &g_app->camera, &g_app->scene, @@ -697,6 +862,9 @@ #endif Canvas_App_Draw_Listening_Indicator(); EndDrawing(); +#if defined(INFINITE_CANVAS_DEV_UI) + Canvas_App_Handle_Dev_Action(dev_action); +#endif g_app->frame_count++; #if !defined(PLATFORM_WEB) if (g_app->frame_count == 30) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/scene_store.c Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,569 @@ +#include "infinite_canvas/scene_store.h" + +#include <ctype.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if defined(_WIN32) +#include <windows.h> +#elif !defined(PLATFORM_WEB) +#include <unistd.h> +#endif + +#define CANVAS_SCENE_STORE_VERSION 1 +#define CANVAS_SCENE_STORE_HEADER_SIZE 48 +#define CANVAS_SCENE_STORE_ENTITY_SIZE 2240 +#define CANVAS_SCENE_STORE_MAX_BYTES \ + (CANVAS_SCENE_STORE_HEADER_SIZE + \ + CANVAS_MAX_ENTITIES * CANVAS_SCENE_STORE_ENTITY_SIZE) + +static const uint8 CANVAS_SCENE_STORE_MAGIC[8] = { + 'Z', 'E', 'N', 'B', 'U', 'M', 'A', 'P', +}; + +_Static_assert(sizeof(Color) == 4, "scene format requires RGBA8 Color"); +_Static_assert( + CANVAS_ENTITY_TEXT_CAPACITY == 2048, + "scene format version must change when text capacity changes"); +_Static_assert( + CANVAS_SCENE_STORE_ENTITY_SIZE == + 2 * 4 + 4 * 4 + 4 + 128 + 2048 + 3 * 4 + 6 * 4, + "scene entity record size is inconsistent"); + +typedef struct { + uint8 *p_data; + size_t size; + size_t offset; + boolean valid; +} Canvas_Scene_Writer; + +typedef struct { + const uint8 *p_data; + size_t size; + size_t offset; + boolean valid; +} Canvas_Scene_Reader; + +static uint64 Canvas_Scene_Store_FNV( + uint64 hash, + const uint8 *p_data, + size_t size) +{ + for (size_t index = 0; index < size; index++) { + hash ^= p_data[index]; + hash *= 1099511628211ULL; + } + return hash; +} + +static void Canvas_Scene_Store_Write( + Canvas_Scene_Writer *p_writer, + const void *p_data, + size_t size) +{ + if (!p_writer->valid || + p_writer->offset > p_writer->size || + size > p_writer->size - p_writer->offset) { + p_writer->valid = FALSE; + return; + } + memcpy(p_writer->p_data + p_writer->offset, p_data, size); + p_writer->offset += size; +} + +static void Canvas_Scene_Store_Write_U32( + Canvas_Scene_Writer *p_writer, + uint32 value) +{ + uint8 bytes[4] = { + (uint8)(value & 0xff), + (uint8)((value >> 8) & 0xff), + (uint8)((value >> 16) & 0xff), + (uint8)((value >> 24) & 0xff), + }; + Canvas_Scene_Store_Write(p_writer, bytes, sizeof(bytes)); +} + +static void Canvas_Scene_Store_Write_F32( + Canvas_Scene_Writer *p_writer, + float value) +{ + uint32 bits = 0; + memcpy(&bits, &value, sizeof(bits)); + Canvas_Scene_Store_Write_U32(p_writer, bits); +} + +static void Canvas_Scene_Store_Read( + Canvas_Scene_Reader *p_reader, + void *p_output, + size_t size) +{ + if (!p_reader->valid || + p_reader->offset > p_reader->size || + size > p_reader->size - p_reader->offset) { + p_reader->valid = FALSE; + return; + } + memcpy(p_output, p_reader->p_data + p_reader->offset, size); + p_reader->offset += size; +} + +static uint32 Canvas_Scene_Store_Read_U32( + Canvas_Scene_Reader *p_reader) +{ + uint8 bytes[4] = {0}; + Canvas_Scene_Store_Read(p_reader, bytes, sizeof(bytes)); + return (uint32)bytes[0] | + ((uint32)bytes[1] << 8) | + ((uint32)bytes[2] << 16) | + ((uint32)bytes[3] << 24); +} + +static float Canvas_Scene_Store_Read_F32( + Canvas_Scene_Reader *p_reader) +{ + uint32 bits = Canvas_Scene_Store_Read_U32(p_reader); + float value = 0.0f; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static uint32 Canvas_Scene_Store_Entity_Flags( + const Canvas_Entity *p_entity) +{ + uint32 flags = 0; + if (p_entity->active) flags |= 1u << 0; + if (p_entity->pinned) flags |= 1u << 1; + if (p_entity->text_muted) flags |= 1u << 2; + if (p_entity->parked) flags |= 1u << 3; + return flags; +} + +static void Canvas_Scene_Store_Write_Entity( + Canvas_Scene_Writer *p_writer, + const Canvas_Entity *p_entity) +{ + Canvas_Scene_Store_Write_U32(p_writer, p_entity->id); + Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->type); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.x); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->position.y); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.x); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->size.y); + Canvas_Scene_Store_Write(p_writer, &p_entity->color, 4); + Canvas_Scene_Store_Write( + p_writer, p_entity->label, sizeof(p_entity->label)); + Canvas_Scene_Store_Write( + p_writer, p_entity->text, sizeof(p_entity->text)); + Canvas_Scene_Store_Write_U32(p_writer, (uint32)p_entity->value); + Canvas_Scene_Store_Write_F32(p_writer, p_entity->text_scroll_y); + Canvas_Scene_Store_Write_U32( + p_writer, Canvas_Scene_Store_Entity_Flags(p_entity)); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_position.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_position.y); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_size.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->pinned_screen_size.y); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->parked_restore_position.x); + Canvas_Scene_Store_Write_F32( + p_writer, p_entity->parked_restore_position.y); +} + +static Canvas_Entity Canvas_Scene_Store_Read_Entity( + Canvas_Scene_Reader *p_reader) +{ + Canvas_Entity entity = {0}; + entity.id = Canvas_Scene_Store_Read_U32(p_reader); + entity.type = + (Canvas_Entity_Type)Canvas_Scene_Store_Read_U32(p_reader); + entity.position.x = Canvas_Scene_Store_Read_F32(p_reader); + entity.position.y = Canvas_Scene_Store_Read_F32(p_reader); + entity.size.x = Canvas_Scene_Store_Read_F32(p_reader); + entity.size.y = Canvas_Scene_Store_Read_F32(p_reader); + Canvas_Scene_Store_Read(p_reader, &entity.color, 4); + Canvas_Scene_Store_Read( + p_reader, entity.label, sizeof(entity.label)); + Canvas_Scene_Store_Read( + p_reader, entity.text, sizeof(entity.text)); + entity.label[sizeof(entity.label) - 1] = '\0'; + entity.text[sizeof(entity.text) - 1] = '\0'; + entity.value = (int32)Canvas_Scene_Store_Read_U32(p_reader); + entity.text_scroll_y = Canvas_Scene_Store_Read_F32(p_reader); + uint32 flags = Canvas_Scene_Store_Read_U32(p_reader); + entity.active = flags & (1u << 0) ? TRUE : FALSE; + entity.pinned = flags & (1u << 1) ? TRUE : FALSE; + entity.text_muted = flags & (1u << 2) ? TRUE : FALSE; + entity.parked = flags & (1u << 3) ? TRUE : FALSE; + entity.pinned_screen_position.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_position.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_size.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.pinned_screen_size.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.parked_restore_position.x = + Canvas_Scene_Store_Read_F32(p_reader); + entity.parked_restore_position.y = + Canvas_Scene_Store_Read_F32(p_reader); + entity.text_cursor = (int32)strlen(entity.text); + entity.text_selection_anchor = entity.text_cursor; + entity.visibility_amount = 1.0f; + entity.screen_space_initialized = entity.pinned; + entity.animation_amount = + entity.type == CANVAS_ENTITY_CONVERSATION + ? (entity.active ? 0.0f : 1.0f) + : (entity.active ? 1.0f : 0.0f); + return entity; +} + +static boolean Canvas_Scene_Store_Entity_Valid( + const Canvas_Entity *p_entity) +{ + boolean base_valid = p_entity->id != 0 && + p_entity->type >= 0 && + p_entity->type < CANVAS_ENTITY_TYPE_COUNT && + isfinite(p_entity->position.x) && + isfinite(p_entity->position.y) && + isfinite(p_entity->size.x) && + isfinite(p_entity->size.y) && + p_entity->size.x > 0.0f && + p_entity->size.y > 0.0f && + fabsf(p_entity->position.x) <= 1000000000.0f && + fabsf(p_entity->position.y) <= 1000000000.0f && + p_entity->size.x <= 10000000.0f && + p_entity->size.y <= 10000000.0f; + if (!base_valid) return FALSE; + if (p_entity->pinned && + (!isfinite(p_entity->pinned_screen_position.x) || + !isfinite(p_entity->pinned_screen_position.y) || + !isfinite(p_entity->pinned_screen_size.x) || + !isfinite(p_entity->pinned_screen_size.y) || + p_entity->pinned_screen_size.x <= 0.0f || + p_entity->pinned_screen_size.y <= 0.0f)) { + return FALSE; + } + return !p_entity->parked || + (isfinite(p_entity->parked_restore_position.x) && + isfinite(p_entity->parked_restore_position.y)); +} + +boolean Canvas_Scene_Store_Path( + const char *p_name, + char *p_path, + size_t path_capacity) +{ +#if defined(PLATFORM_WEB) + (void)p_name; + (void)p_path; + (void)path_capacity; + return FALSE; +#else + if (!p_name || !p_name[0] || !p_path || path_capacity == 0) { + return FALSE; + } + size_t name_length = strlen(p_name); + if (name_length >= CANVAS_SCENE_STORE_NAME_CAPACITY) return FALSE; + for (size_t index = 0; index < name_length; index++) { + uint8 character = (uint8)p_name[index]; + if (!isalnum(character) && + character != '-' && + character != '_') { + return FALSE; + } + } + const char *p_root = getenv("XDG_STATE_HOME"); + const char *p_suffix = "/zenbu/infinite-canvas/snapshots"; + char fallback[CANVAS_SCENE_STORE_PATH_CAPACITY]; + if (!p_root || !p_root[0]) { + const char *p_home = getenv("HOME"); + if (!p_home || !p_home[0]) return FALSE; + int fallback_written = snprintf( + fallback, + sizeof(fallback), + "%s/.local/state", + p_home); + if (fallback_written <= 0 || + (size_t)fallback_written >= sizeof(fallback)) { + return FALSE; + } + p_root = fallback; + } + int written = snprintf( + p_path, + path_capacity, + "%s%s/%s.zmap", + p_root, + p_suffix, + p_name); + return written > 0 && (size_t)written < path_capacity; +#endif +} + +boolean Canvas_Scene_Store_Save( + const char *p_path, + const Canvas_Scene *p_scene, + const Canvas_Camera *p_camera) +{ +#if defined(PLATFORM_WEB) + (void)p_path; + (void)p_scene; + (void)p_camera; + return FALSE; +#else + if (!p_path || !p_scene || !p_camera) return FALSE; + size_t entity_count = 0; + for (size_t index = 0; + index < Dowa_Array_Length(p_scene->p_entities); + index++) { + if (!p_scene->p_entities[index].removing) entity_count++; + } + if (entity_count > CANVAS_MAX_ENTITIES) return FALSE; + size_t size = CANVAS_SCENE_STORE_HEADER_SIZE + + entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE; + uint8 *p_data = (uint8 *)calloc(size, 1); + if (!p_data) return FALSE; + Canvas_Scene_Writer writer = { + .p_data = p_data, + .size = size, + .valid = TRUE, + }; + Canvas_Scene_Store_Write( + &writer, CANVAS_SCENE_STORE_MAGIC, sizeof(CANVAS_SCENE_STORE_MAGIC)); + Canvas_Scene_Store_Write_U32(&writer, CANVAS_SCENE_STORE_VERSION); + Canvas_Scene_Store_Write_U32(&writer, (uint32)entity_count); + Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity); + Canvas_Scene_Store_Write_U32(&writer, p_scene->next_entity_id); + Canvas_Scene_Store_Write_U32( + &writer, p_scene->show_grid ? 1u : 0u); + Canvas_Scene_Store_Write_F32(&writer, p_camera->target.x); + Canvas_Scene_Store_Write_F32(&writer, p_camera->target.y); + Canvas_Scene_Store_Write_F32(&writer, p_camera->zoom); + Canvas_Scene_Store_Write_U32( + &writer, (uint32)(size - CANVAS_SCENE_STORE_HEADER_SIZE)); + Canvas_Scene_Store_Write_U32(&writer, 0); + for (size_t index = 0; + index < Dowa_Array_Length(p_scene->p_entities); + index++) { + if (!p_scene->p_entities[index].removing) { + Canvas_Scene_Store_Write_Entity( + &writer, &p_scene->p_entities[index]); + } + } + if (!writer.valid || writer.offset != size) { + Dowa_Free(p_data); + return FALSE; + } + uint64 checksum_hash = Canvas_Scene_Store_FNV( + 1469598103934665603ULL, + p_data, + 44); + checksum_hash = Canvas_Scene_Store_FNV( + checksum_hash, + p_data + CANVAS_SCENE_STORE_HEADER_SIZE, + size - CANVAS_SCENE_STORE_HEADER_SIZE); + uint32 checksum = (uint32)checksum_hash; + p_data[44] = (uint8)(checksum & 0xff); + p_data[45] = (uint8)((checksum >> 8) & 0xff); + p_data[46] = (uint8)((checksum >> 16) & 0xff); + p_data[47] = (uint8)((checksum >> 24) & 0xff); + + char directory[CANVAS_SCENE_STORE_PATH_CAPACITY]; + char temporary[CANVAS_SCENE_STORE_PATH_CAPACITY]; + snprintf(directory, sizeof(directory), "%s", p_path); + char *p_separator = strrchr(directory, '/'); +#if defined(_WIN32) + char *p_windows_separator = strrchr(directory, '\\'); + if (!p_separator || p_windows_separator > p_separator) { + p_separator = p_windows_separator; + } +#endif + if (!p_separator) { + Dowa_Free(p_data); + return FALSE; + } + *p_separator = '\0'; + if (MakeDirectory(directory) != 0 && !DirectoryExists(directory)) { + Dowa_Free(p_data); + return FALSE; + } + int temporary_written = snprintf( + temporary, sizeof(temporary), "%s.tmp", p_path); + if (temporary_written <= 0 || + (size_t)temporary_written >= sizeof(temporary)) { + Dowa_Free(p_data); + return FALSE; + } + FILE *p_file = fopen(temporary, "wb"); + if (!p_file) { + Dowa_Free(p_data); + return FALSE; + } + boolean success = + fwrite(p_data, 1, size, p_file) == size && + fflush(p_file) == 0; +#if !defined(_WIN32) + if (success) success = fsync(fileno(p_file)) == 0; +#endif + if (fclose(p_file) != 0) success = FALSE; + Dowa_Free(p_data); + if (!success) { + remove(temporary); + return FALSE; + } +#if defined(_WIN32) + success = MoveFileExA( + temporary, + p_path, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) + ? TRUE + : FALSE; +#else + success = rename(temporary, p_path) == 0 ? TRUE : FALSE; +#endif + if (!success) remove(temporary); + return success; +#endif +} + +boolean Canvas_Scene_Store_Load( + const char *p_path, + Canvas_Scene *p_scene, + Canvas_Camera *p_camera) +{ +#if defined(PLATFORM_WEB) + (void)p_path; + (void)p_scene; + (void)p_camera; + return FALSE; +#else + if (!p_path || !p_scene || !p_camera) return FALSE; + FILE *p_file = fopen(p_path, "rb"); + if (!p_file) return FALSE; + if (fseek(p_file, 0, SEEK_END) != 0) { + fclose(p_file); + return FALSE; + } + long file_size = ftell(p_file); + if (file_size < CANVAS_SCENE_STORE_HEADER_SIZE || + file_size > CANVAS_SCENE_STORE_MAX_BYTES || + fseek(p_file, 0, SEEK_SET) != 0) { + fclose(p_file); + return FALSE; + } + uint8 *p_data = (uint8 *)malloc((size_t)file_size); + if (!p_data) { + fclose(p_file); + return FALSE; + } + boolean success = + fread(p_data, 1, (size_t)file_size, p_file) == (size_t)file_size; + fclose(p_file); + if (!success) { + Dowa_Free(p_data); + return FALSE; + } + + Canvas_Scene_Reader reader = { + .p_data = p_data, + .size = (size_t)file_size, + .valid = TRUE, + }; + uint8 magic[8] = {0}; + Canvas_Scene_Store_Read(&reader, magic, sizeof(magic)); + uint32 version = Canvas_Scene_Store_Read_U32(&reader); + uint32 entity_count = Canvas_Scene_Store_Read_U32(&reader); + uint32 next_entity = Canvas_Scene_Store_Read_U32(&reader); + uint32 next_entity_id = Canvas_Scene_Store_Read_U32(&reader); + uint32 scene_flags = Canvas_Scene_Store_Read_U32(&reader); + float camera_x = Canvas_Scene_Store_Read_F32(&reader); + float camera_y = Canvas_Scene_Store_Read_F32(&reader); + float camera_zoom = Canvas_Scene_Store_Read_F32(&reader); + uint32 payload_size = Canvas_Scene_Store_Read_U32(&reader); + uint32 stored_checksum = Canvas_Scene_Store_Read_U32(&reader); + size_t expected_size = CANVAS_SCENE_STORE_HEADER_SIZE + + (size_t)entity_count * CANVAS_SCENE_STORE_ENTITY_SIZE; + uint64 checksum_hash = Canvas_Scene_Store_FNV( + 1469598103934665603ULL, + p_data, + 44); + checksum_hash = Canvas_Scene_Store_FNV( + checksum_hash, + p_data + CANVAS_SCENE_STORE_HEADER_SIZE, + (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE); + uint32 actual_checksum = (uint32)checksum_hash; + if (!reader.valid || + memcmp(magic, CANVAS_SCENE_STORE_MAGIC, sizeof(magic)) != 0 || + version != CANVAS_SCENE_STORE_VERSION || + entity_count > CANVAS_MAX_ENTITIES || + expected_size != (size_t)file_size || + payload_size != (uint32)( + (size_t)file_size - CANVAS_SCENE_STORE_HEADER_SIZE) || + stored_checksum != actual_checksum || + next_entity == 0xffffffffu || + next_entity_id == 0xffffffffu || + !isfinite(camera_x) || + !isfinite(camera_y) || + !isfinite(camera_zoom) || + fabsf(camera_x) > 1000000000.0f || + fabsf(camera_y) > 1000000000.0f || + camera_zoom < p_camera->min_zoom || + camera_zoom > p_camera->max_zoom) { + Dowa_Free(p_data); + return FALSE; + } + + Canvas_Entity *p_entities = (Canvas_Entity *)calloc( + entity_count ? entity_count : 1, + sizeof(*p_entities)); + if (!p_entities) { + Dowa_Free(p_data); + return FALSE; + } + uint32 max_id = 0; + for (uint32 index = 0; index < entity_count; index++) { + p_entities[index] = Canvas_Scene_Store_Read_Entity(&reader); + if (!reader.valid || + !Canvas_Scene_Store_Entity_Valid(&p_entities[index])) { + Dowa_Free(p_entities); + Dowa_Free(p_data); + return FALSE; + } + for (uint32 previous = 0; previous < index; previous++) { + if (p_entities[previous].id == p_entities[index].id) { + Dowa_Free(p_entities); + Dowa_Free(p_data); + return FALSE; + } + } + if (p_entities[index].id > max_id) { + max_id = p_entities[index].id; + } + } + Dowa_Free(p_data); + if (!reader.valid || reader.offset != (size_t)file_size) { + Dowa_Free(p_entities); + return FALSE; + } + + Canvas_Scene_Clear(p_scene); + for (uint32 index = 0; index < entity_count; index++) { + Dowa_Array_Push_Arena( + p_scene->p_entities, p_entities[index], p_scene->p_arena); + } + Dowa_Free(p_entities); + p_scene->next_entity = + next_entity < entity_count ? entity_count : next_entity; + p_scene->next_entity_id = + next_entity_id < max_id ? max_id : next_entity_id; + p_scene->show_grid = scene_flags & 1u ? TRUE : FALSE; + p_camera->target = (Vector2){camera_x, camera_y}; + p_camera->zoom = camera_zoom; + return TRUE; +#endif +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/scene_store.h Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,22 @@ +#ifndef INFINITE_CANVAS_SCENE_STORE_H +#define INFINITE_CANVAS_SCENE_STORE_H + +#include "infinite_canvas/canvas.h" + +#define CANVAS_SCENE_STORE_NAME_CAPACITY 48 +#define CANVAS_SCENE_STORE_PATH_CAPACITY 1024 + +boolean Canvas_Scene_Store_Path( + const char *p_name, + char *p_path, + size_t path_capacity); +boolean Canvas_Scene_Store_Save( + const char *p_path, + const Canvas_Scene *p_scene, + const Canvas_Camera *p_camera); +boolean Canvas_Scene_Store_Load( + const char *p_path, + Canvas_Scene *p_scene, + Canvas_Camera *p_camera); + +#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/infinite_canvas/scene_store_test.c Tue Aug 18 22:18:15 2026 -0700 @@ -0,0 +1,97 @@ +#include "infinite_canvas/scene_store.h" + +#include <assert.h> +#include <stdio.h> +#include <string.h> +#if defined(_WIN32) +#include <windows.h> +#else +#include <unistd.h> +#endif + +int main(void) +{ + char path[256]; +#if defined(_WIN32) + snprintf( + path, + sizeof(path), + "C:/temp/infinite-canvas-scene-%lu.zmap", + (unsigned long)GetCurrentProcessId()); +#else + snprintf( + path, + sizeof(path), + "/tmp/infinite-canvas-scene-%ld.zmap", + (long)getpid()); +#endif + char named_path[CANVAS_SCENE_STORE_PATH_CAPACITY]; +#if defined(_WIN32) + assert(_putenv_s("XDG_STATE_HOME", "C:/temp/zenbu-scene-store-test") == 0); +#else + assert(setenv("XDG_STATE_HOME", "/tmp/zenbu-scene-store-test", 1) == 0); +#endif + assert(Canvas_Scene_Store_Path( + "workspace-1", named_path, sizeof(named_path))); + assert(strstr(named_path, "workspace-1.zmap")); + assert(!Canvas_Scene_Store_Path( + "../workspace", named_path, sizeof(named_path))); + Dowa_Arena *p_source_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE); + Dowa_Arena *p_loaded_arena = Dowa_Arena_Create(4 * ONE_MEGA_BYTE); + assert(p_source_arena && p_loaded_arena); + Canvas_Scene source; + Canvas_Scene loaded; + Canvas_Camera source_camera; + Canvas_Camera loaded_camera; + Canvas_Scene_Init(&source, p_source_arena); + Canvas_Scene_Init(&loaded, p_loaded_arena); + Canvas_Camera_Init(&source_camera, 1200, 800); + Canvas_Camera_Init(&loaded_camera, 1200, 800); + + assert(Canvas_Scene_Add( + &source, CANVAS_ENTITY_TEXT_AREA, (Vector2){42.0f, -17.0f})); + assert(Canvas_Scene_Add( + &source, CANVAS_ENTITY_CONVERSATION, (Vector2){800.0f, 120.0f})); + Canvas_Entity *p_text = &source.p_entities[0]; + snprintf(p_text->label, sizeof(p_text->label), "Dictation"); + snprintf(p_text->text, sizeof(p_text->text), "persist me"); + p_text->pinned = TRUE; + p_text->pinned_screen_position = (Vector2){30.0f, 40.0f}; + p_text->pinned_screen_size = (Vector2){280.0f, 148.0f}; + Canvas_Entity *p_conversation = &source.p_entities[1]; + p_conversation->active = TRUE; + p_conversation->parked = TRUE; + p_conversation->parked_restore_position = (Vector2){90.0f, 110.0f}; + source.show_grid = TRUE; + source_camera.target = (Vector2){400.0f, -200.0f}; + source_camera.zoom = 1.75f; + + assert(Canvas_Scene_Store_Save(path, &source, &source_camera)); + assert(Canvas_Scene_Store_Load(path, &loaded, &loaded_camera)); + assert(Dowa_Array_Length(loaded.p_entities) == 2); + assert(strcmp(loaded.p_entities[0].label, "Dictation") == 0); + assert(strcmp(loaded.p_entities[0].text, "persist me") == 0); + assert(loaded.p_entities[0].pinned); + assert(loaded.p_entities[1].active); + assert(loaded.p_entities[1].parked); + assert(loaded.show_grid); + assert(loaded_camera.target.x == 400.0f); + assert(loaded_camera.target.y == -200.0f); + assert(loaded_camera.zoom == 1.75f); + + FILE *p_file = fopen(path, "r+b"); + assert(p_file); + assert(fseek(p_file, 50, SEEK_SET) == 0); + int byte = fgetc(p_file); + assert(byte != EOF); + assert(fseek(p_file, 50, SEEK_SET) == 0); + assert(fputc(byte ^ 0xff, p_file) != EOF); + fclose(p_file); + assert(!Canvas_Scene_Store_Load(path, &loaded, &loaded_camera)); + assert(Dowa_Array_Length(loaded.p_entities) == 2); + + remove(path); + Dowa_Arena_Free(p_loaded_arena); + Dowa_Arena_Free(p_source_arena); + return 0; +}
--- a/mrjunejune/inference/copilot_sidecar.py Tue Aug 18 19:14:53 2026 -0700 +++ b/mrjunejune/inference/copilot_sidecar.py Tue Aug 18 22:18:15 2026 -0700 @@ -34,12 +34,34 @@ The user message includes only the visible canvas context plus one new thought. Treat visible conversation entities as the only append candidates. Append when the new thought clearly continues one of them; otherwise create a new -conversation. Never append to a conversation ID absent from visible context. +conversation only when the request benefits from retained discussion. Never +append to a conversation ID absent from visible context. Return only one compact JSON object with these fields: - action: "create" or "append" +- presentation: "action" for a one-off canvas action, or "conversation" for + retained discussion - conversation_id: an existing numeric conversation ID for append, otherwise 0 - title: a short session title - response: the direct response that should appear on the canvas +- entities: an array of zero to four useful visual entities to create +Each entities item may contain: +- kind: one of "text", "button", "text_area", "accordion", "card", + "calendar", "table", "notification", "scroll_area", "image", or + "web_content" +- label and text: short display strings; image/web_content text must be an + absolute http:// or https:// URL +- x and y: optional world coordinates; omit both to let the canvas place it +- width and height: optional dimensions; omit both for the native default +- value: optional integer state; for a calendar it is the 1-based selected + day of the month +Create entities only when they materially demonstrate or organize the answer. +Never create more than four. Do not create conversation entities through this +array; action and conversation_id own the conversation lifecycle. +For a simple one-off task, use presentation "action", action "create", +conversation_id 0, and at least one useful entity. Handle it directly in this +orchestrator: do not create or delegate to another agent, worker, or session. +For a complex answer that should remain discussable, use presentation +"conversation". Append always requires presentation "conversation". Do not use Markdown fences, tools, or text outside the JSON object. """ @@ -305,6 +327,38 @@ } await self._send("ready", None, None, status="ok", profiles=profile_meta) + def _resume_marker(self, session_id: str) -> str: + return os.path.join( + self._config.base_directory, + ".session-markers", + session_id, + ) + + def _should_attempt_resume(self, session_id: str) -> bool: + if not os.path.isdir(self._config.base_directory): + return True + return os.path.isfile(self._resume_marker(session_id)) + + def _mark_session_created(self, session_id: str) -> None: + marker = self._resume_marker(session_id) + try: + os.makedirs(os.path.dirname(marker), mode=0o700, exist_ok=True) + with open(marker, "wb"): + pass + except OSError as error: + print( + f"unable to write Copilot session marker: {error}", + file=sys.stderr, + ) + + def _remove_resume_marker(self, session_id: str) -> None: + try: + os.remove(self._resume_marker(session_id)) + except FileNotFoundError: + pass + except OSError: + pass + async def dispatch(self, command: JsonObject) -> None: command_name = command.get("command") request_id = command.get("request_id") @@ -582,12 +636,12 @@ base_options = self._session_options(compiled) session: Any = None - if resume_existing: + if resume_existing and self._should_attempt_resume(derived_id): # Resume persisted user-facing sessions, then create if absent. try: session = await self._client.resume_session(derived_id, **base_options) except Exception: - pass + self._remove_resume_marker(derived_id) if session is None: # Fresh create — inject bounded transcript context into system message. @@ -599,6 +653,7 @@ session = await self._client.create_session( session_id=derived_id, **create_options ) + self._mark_session_created(derived_id) def handle_event(event: Any) -> None: task = asyncio.create_task(self._handle_event(conversation_id, event)) @@ -758,6 +813,7 @@ conversation.unsubscribe() await conversation.session.disconnect() await self._client.delete_session(conversation.session.session_id) + self._remove_resume_marker(conversation.session.session_id) deleted_ids.add(conversation.session.session_id) for compiled in self._compiled.values(): sdk_session_id = _derive_sdk_session_id(conversation_id, compiled) @@ -768,6 +824,7 @@ except Exception: # A profile-specific persisted session may never have existed. pass + self._remove_resume_marker(sdk_session_id) await self._send( "turn.done", request_id, conversation_id, action="conversation.delete" )
--- a/mrjunejune/inference/copilot_sidecar_test.py Tue Aug 18 19:14:53 2026 -0700 +++ b/mrjunejune/inference/copilot_sidecar_test.py Tue Aug 18 22:18:15 2026 -0700 @@ -1,5 +1,7 @@ import asyncio import hashlib +import os +import tempfile import types import unittest import uuid @@ -242,6 +244,37 @@ ["session.warmed", "turn.done"], ) + async def test_warm_skips_resume_without_persistence_marker(self): + with tempfile.TemporaryDirectory() as state_directory: + self.config = replace( + self.config, + base_directory=state_directory, + ) + sidecar, client = await self.make_sidecar() + await sidecar.dispatch( + { + "command": "conversation.warm", + "request_id": "warm-persistent", + "conversation_id": "canvas-orchestrator", + "prompt_profile": "canvas_orchestrator", + "prompt_version": 1, + "knowledge_version": 1, + } + ) + + self.assertEqual(len(client.resume_calls), 0) + self.assertEqual(len(client.create_calls), 1) + session_id = client.create_calls[0][0] + self.assertTrue( + os.path.isfile( + os.path.join( + state_directory, + ".session-markers", + session_id, + ) + ) + ) + async def test_warm_rejects_non_boolean_resume_existing(self): sidecar, client = await self.make_sidecar() await sidecar.dispatch(