1510 lines · 61.3 KB · Zig
RawDownload
const std = @import("std");const paste_blocks = @import("../input/pasted_blocks.zig");const registered_entities = @import("../input/registered_entities.zig");const image_attachments = @import("../images/image_attachments.zig");const command_specs = @import("../slash_commands/command_specs.zig");const io_mod = @import("../shared/io.zig");const debug_trace = @import("../shared/debug_trace.zig");const entity_spans = @import("../shared/entity_spans.zig");const types = @import("../shared/types.zig");const input_queue_runtime = @import("input_queue_runtime.zig");const input_completion_runtime = @import("input_completion_runtime.zig");const input_limit_feedback = @import("input_limit_feedback.zig"); pub fn SubmitRuntime(comptime App: type) type {    return struct {        const queue_rt = input_queue_runtime.Runtime(App);        const completion_rt = input_completion_runtime.CompletionRuntime(App);         const AcceptedDraftProjection = struct {            input: []const u8,            pasted_blocks: std.ArrayList(paste_blocks.PastedBlock) = .empty,            image_tokens: std.ArrayList(entity_spans.ImageTokenSpan) = .empty,            skill_tokens: std.ArrayList(registered_entities.SkillTokenSpan) = .empty,             fn deinit(self: *AcceptedDraftProjection, alloc: std.mem.Allocator) void {                self.pasted_blocks.deinit(alloc);                self.image_tokens.deinit(alloc);                self.skill_tokens.deinit(alloc);                self.* = undefined;            }        };         pub fn submitInput(app: *App, max_prompt_history: usize) !void {            try submit(app, max_prompt_history);        }         pub fn submit(app: *App, max_prompt_history: usize) !void {            const expanded_len = paste_blocks.expandedLen(                app.input_runtime.edit_state.input.items,                app.input_runtime.entities.pasted_blocks.items,            ) orelse std.math.maxInt(usize);            const max_input_len = if (comptime @hasDecl(App, "input_limits"))                App.input_limits.composer_bytes            else if (comptime @hasDecl(App, "input_byte_limit"))                App.input_byte_limit            else                std.math.maxInt(usize);            if (expanded_len > max_input_len) {                try input_limit_feedback.report(                    App,                    app,                    .composer,                    expanded_len,                );                return;            }             const expanded = try paste_blocks.expand(app.alloc, app.input_runtime.edit_state.input.items, app.input_runtime.entities.pasted_blocks.items);            defer if (expanded.owned) app.alloc.free(expanded.text);             if (directCommand(expanded.text)) |command| {                if (comptime @hasDecl(App, "submitDirectTerminal")) {                    try App.submitDirectTerminal(app, command);                    return;                }            }             const left_trimmed = std.mem.trimStart(u8, expanded.text, " \t\r\n");            const resolved_slash_submission = resolvedSlashSubmission(app, left_trimmed);            if (knownSlashCommand(app, resolved_slash_submission)) |command| {                if (requiresPromptCredential(command, resolved_slash_submission) and !try preflightPrompt(app)) return;                try submitSlashCommand(app, left_trimmed, null, max_prompt_history);                return;            }            if (shouldRouteUnknownSlashCommand(app, resolved_slash_submission)) {                try submitSlashCommand(app, left_trimmed, null, max_prompt_history);                return;            }            if (pendingImagesPrefixEnd(                app.input_runtime.edit_state.input.items,                expanded.text,                app.input_runtime.entities.pasted_blocks.items,                app.input_runtime.entities.image_tokens.items,                app.pending_images.items,            )) |command_start| {                const command_text = expanded.text[command_start..];                if (knownSlashCommand(app, command_text)) |command| {                    if (requiresPromptCredential(command, command_text) and !try preflightPrompt(app)) return;                    try submitSlashCommand(                        app,                        command_text,                        expanded.text[0..command_start],                        max_prompt_history,                    );                    return;                }            }             const trimmed = std.mem.trim(u8, expanded.text, " \t\r\n");             if (trimmed.len == 0) {                if (app.pending_images.items.len > 0) {                    if (!try preflightPrompt(app)) return;                    if (!try enqueuePromptForSubmit(app, "", &.{}, null)) return;                    releasePendingImages(app);                    app.input_runtime.inputResetState().clearCurrent(app.alloc);                    if (acceptedPromptNeedsImmediateFooter(app)) {                        app.shell.render_requests.request(.footer);                    }                    return;                }                if (comptime @hasField(App, "queued_prompt_review")) {                    if (try queue_rt.submitPausedQueueUnchanged(app)) {                        beginIdleSubmittedPromptTransition(app);                        return;                    }                }                app.input_runtime.inputResetState().clearCurrent(app.alloc);                app.shell.render_requests.request(.footer);                return;            }             const first_image_id = try firstAvailableImageId(app.pending_images.items);            var extracted = image_attachments.extractInlineImageAttachments(                app.alloc,                app.workspace_root,                trimmed,                first_image_id,            ) catch |err| {                const message = switch (err) {                    error.UnsupportedImageType => try app.alloc.dupe(u8, "unsupported image type"),                    error.FileNotFound => try app.alloc.dupe(u8, "image file not found"),                    error.ImageTooLarge => try app.alloc.dupe(u8, image_attachments.image_too_large_notice),                    else => try std.fmt.allocPrint(app.alloc, "failed to attach image: {s}", .{@errorName(err)}),                };                defer app.alloc.free(message);                try app.writeDomainNotice(.{                    .topic = "images",                    .tone = .@"error",                    .body = message,                }, true);                app.shell.render_requests.request(.footer);                return;            };            var discard_extracted = true;            defer if (discard_extracted)                extracted.discard(app.alloc)            else                extracted.deinit(app.alloc);            try assignStableExtractedImageIds(app, &extracted);            if (try captureExtractedImages(app, extracted.images) == .rejected) {                app.shell.render_requests.request(.footer);                return;            }             const has_inline_images = extracted.images.len > 0;            const effective_text = if (has_inline_images) extracted.text else expanded.text;             if (!try preflightPrompt(app)) return;             var staged_images = if (app.pending_images.items.len > 0 or extracted.images.len > 0)                try stagePendingImages(app.alloc, app.pending_images.items, extracted.images)            else                null;            defer if (staged_images) |*images| deinitOwnedImageList(app.alloc, images);             var image_occurrences = try projectImageOccurrencesForSubmit(                app.alloc,                app.input_runtime.edit_state.input.items,                expanded.text,                trimmed,                effective_text,                app.input_runtime.entities.pasted_blocks.items,                app.input_runtime.entities.image_tokens.items,                app.pending_images.items,                extracted.image_spans,                extracted.images,                has_inline_images,            );            defer image_occurrences.deinit(app.alloc);             // One mapping owns both placeholder IDs and attachment order.            const visual_text = if (staged_images) |*images|                try mapSubmittedImages(                    app.alloc,                    effective_text,                    images,                    image_occurrences.items,                    firstRemappedImageId(app),                )            else                VisualText{ .text = effective_text, .owned = false };            defer if (visual_text.owned) app.alloc.free(visual_text.text);            const display_skill_tokens = try projectSkillTokensForSubmit(                app.alloc,                app.input_runtime.edit_state.input.items,                expanded.text,                trimmed,                effective_text,                visual_text.text,                app.input_runtime.entities.pasted_blocks.items,                app.input_runtime.entities.skill_tokens.items,                has_inline_images,                image_occurrences.items,            );            defer if (display_skill_tokens.len > 0) app.alloc.free(display_skill_tokens);            var accepted_draft = try prepareAcceptedDraftProjection(                app,                expanded.text,                visual_text.text,                display_skill_tokens,                image_occurrences.items,                has_inline_images,            );            defer accepted_draft.deinit(app.alloc);            const has_images_for_submit = if (staged_images) |*images|                images.items.len > 0            else                app.pending_images.items.len > 0;             const queued = if (staged_images) |*images|                try enqueuePromptWithStagedImages(                    app,                    visual_text.text,                    display_skill_tokens,                    &accepted_draft,                    images,                )            else                try enqueuePromptForSubmit(                    app,                    visual_text.text,                    display_skill_tokens,                    &accepted_draft,                );            if (!queued) return;            commitStableExtractedImageIds(app, extracted.images);            commitRemappedImageIds(app, visual_text.next_image_id);            if (visual_text.text.len > 0) {                recordAcceptedPromptComposerHistory(                    app,                    max_prompt_history,                    &accepted_draft,                );            }            if (!has_images_for_submit and visual_text.text.len > 0) {                recordAcceptedInput(app, visual_text.text);            }            discard_extracted = false;            releasePendingImages(app);            app.input_runtime.inputResetState().clearCurrent(app.alloc);            paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks);            if (acceptedPromptNeedsImmediateFooter(app)) {                app.shell.render_requests.request(.footer);            }        }         fn acceptedPromptNeedsImmediateFooter(app: *const App) bool {            if (comptime @hasField(App, "stream")) return app.stream.active;            return true;        }         fn preflightPrompt(app: *App) !bool {            if (comptime @hasDecl(App, "ensurePromptCredential")) {                return App.ensurePromptCredential(app);            }            return true;        }         fn knownSlashCommand(app: *const App, text: []const u8) ?*const command_specs.SlashSpec {            if (text.len == 0 or text[0] != '/') return null;            var command_end: usize = 1;            while (command_end < text.len and !std.ascii.isWhitespace(text[command_end])) : (command_end += 1) {}            return app.slashRegistry().lookup(text[0..command_end]);        }         fn shouldRouteUnknownSlashCommand(app: *App, text: []const u8) bool {            if (text.len == 0 or text[0] != '/') return false;            const command_end = std.mem.indexOfAny(u8, text, " \t\r\n") orelse text.len;            if (command_end != text.len) return false;            if (std.mem.indexOfScalar(u8, text[1..command_end], '/') != null) return false;            if (app.input_runtime.picker.isInlinePickerDismissed(.slash)) return false;            if (app.input_runtime.picker.inlinePickerTriggerKind(&app.input_runtime.edit_state) != .slash) return false;            return completion_rt.slashCompletionCandidateCount(app) == 0;        }         fn requiresPromptCredential(command: *const command_specs.SlashSpec, submission: []const u8) bool {            if (!command.requires_prompt_credential) return false;            if (command.kind != .model) return true;             var command_end: usize = 1;            while (command_end < submission.len and !std.ascii.isWhitespace(submission[command_end])) : (command_end += 1) {}            return std.mem.trim(u8, submission[command_end..], " \t\r\n").len > 0;        }         fn pendingImagesPrefixEnd(            raw_input: []const u8,            expanded_text: []const u8,            pasted_blocks: []const paste_blocks.PastedBlock,            image_tokens: []const entity_spans.ImageTokenSpan,            images: []const types.ImageAttachment,        ) ?usize {            var index: usize = 0;            while (index < expanded_text.len and std.ascii.isWhitespace(expanded_text[index])) : (index += 1) {}             var saw_placeholder = false;            while (projectedImageTokenAt(                raw_input,                pasted_blocks,                image_tokens,                images,                index,            )) |projected| {                saw_placeholder = true;                index = projected.end;                while (index < expanded_text.len and std.ascii.isWhitespace(expanded_text[index])) : (index += 1) {}            }            if (!saw_placeholder) return null;            return index;        }         fn projectedImageTokenAt(            raw_input: []const u8,            pasted_blocks: []const paste_blocks.PastedBlock,            image_tokens: []const entity_spans.ImageTokenSpan,            images: []const types.ImageAttachment,            expanded_start: usize,        ) ?Span {            for (image_tokens) |token| {                if (!validRawImageToken(raw_input, token)) continue;                if (image_attachments.findImageIndexById(images, token.id) == null) continue;                const start = paste_blocks.projectOffsetThroughExpansion(                    raw_input,                    pasted_blocks,                    token.span.raw_start,                ) orelse continue;                if (start < expanded_start) continue;                if (start > expanded_start) return null;                const end = paste_blocks.projectOffsetThroughExpansion(                    raw_input,                    pasted_blocks,                    token.span.raw_end,                ) orelse return null;                return .{ .start = start, .end = end };            }            return null;        }         fn submitSlashCommand(            app: *App,            text: []const u8,            retained_prefix: ?[]const u8,            max_prompt_history: usize,        ) !void {            const command_text = resolvedSlashSubmission(app, text);            const command_copy = try app.alloc.dupe(u8, command_text);            defer app.alloc.free(command_copy);            const prefix_copy: ?[]u8 = if (retained_prefix) |prefix|                try app.alloc.dupe(u8, prefix)            else                null;            defer if (prefix_copy) |prefix| app.alloc.free(prefix);            var retained_image_tokens = if (prefix_copy) |prefix|                try projectRetainedImageTokens(app, prefix.len)            else                std.ArrayList(entity_spans.ImageTokenSpan).empty;            defer retained_image_tokens.deinit(app.alloc);            if (prefix_copy) |prefix| {                try app.input_runtime.edit_state.input.ensureTotalCapacity(app.alloc, prefix.len);                try app.input_runtime.entities.image_tokens.ensureTotalCapacity(                    app.alloc,                    retained_image_tokens.items.len,                );            }             app.input_runtime.inputResetState().clearCurrent(app.alloc);            if (prefix_copy) |prefix| {                app.input_runtime.edit_state.insertSliceAssumeCapacity(prefix);                app.input_runtime.entities.image_tokens.appendSliceAssumeCapacity(                    retained_image_tokens.items,                );            }            paste_blocks.clearBlocks(app.alloc, &app.input_runtime.entities.pasted_blocks);            if (retained_prefix == null and app.pending_images.items.len > 0) {                debug_trace.logf(                    "input",                    "draft images dropped count={d} reason=local_command",                    .{app.pending_images.items.len},                );                app.clearPendingImages();            }            app.shell.render_requests.request(.footer);            defer if (retained_prefix != null) {                if (app.pending_images.items.len == 0) {                    app.input_runtime.inputResetState().clearCurrent(app.alloc);                }                app.shell.render_requests.request(.footer);            };            recordAcceptedSlashCommandHistory(                app,                max_prompt_history,                command_copy,            );            try App.handleCommand(app, command_copy);        }         fn projectRetainedImageTokens(            app: *App,            expanded_prefix_len: usize,        ) !std.ArrayList(entity_spans.ImageTokenSpan) {            var projected: std.ArrayList(entity_spans.ImageTokenSpan) = .empty;            errdefer projected.deinit(app.alloc);            for (app.input_runtime.entities.image_tokens.items) |token| {                if (!validRawImageToken(app.input_runtime.edit_state.input.items, token)) continue;                if (image_attachments.findImageIndexById(                    app.pending_images.items,                    token.id,                ) == null) continue;                const start = paste_blocks.projectOffsetThroughExpansion(                    app.input_runtime.edit_state.input.items,                    app.input_runtime.entities.pasted_blocks.items,                    token.span.raw_start,                ) orelse continue;                const end = paste_blocks.projectOffsetThroughExpansion(                    app.input_runtime.edit_state.input.items,                    app.input_runtime.entities.pasted_blocks.items,                    token.span.raw_end,                ) orelse continue;                if (end > expanded_prefix_len) continue;                try projected.append(app.alloc, .{                    .id = token.id,                    .span = .{ .raw_start = start, .raw_end = end },                });            }            return projected;        }         fn resolvedSlashSubmission(app: *App, text: []const u8) []const u8 {            if (completion_rt.visibleSlashCompletionCount(app) == 0) return text;            return resolveSlashSubmission(app.slashRegistry(), text, app.input_runtime.picker.slash_completion_index);        }         fn enqueuePromptForSubmit(            app: *App,            prompt: []const u8,            skill_tokens: []const registered_entities.SkillTokenSpan,            accepted_draft: ?*const AcceptedDraftProjection,        ) !bool {            const resume_review = if (comptime @hasField(App, "queued_prompt_review"))                app.queued_prompt_review.active()            else                false;            if (comptime @hasField(App, "queued_prompt_review")) {                switch (try queue_rt.promptAdmission(                    app,                    if (accepted_draft) |draft| draft.input else prompt,                    if (accepted_draft) |draft| draft.pasted_blocks.items else &.{},                    if (accepted_draft) |draft| draft.image_tokens.items else &.{},                    if (accepted_draft) |draft| draft.skill_tokens.items else skill_tokens,                )) {                    .replaced => {                        beginIdleSubmittedPromptTransition(app);                        return true;                    },                    .enqueue => {},                }            }             const accepted = if (comptime @hasDecl(App, "enqueuePromptWithReviewDraft")) blk: {                if (accepted_draft) |draft| {                    break :blk try App.enqueuePromptWithReviewDraft(                        app,                        prompt,                        skill_tokens,                        draft.input,                        draft.pasted_blocks.items,                        draft.image_tokens.items,                        draft.skill_tokens.items,                    );                }                break :blk try App.enqueuePromptWithSkillBindings(                    app,                    prompt,                    skill_tokens,                );            } else if (comptime @hasDecl(App, "enqueuePromptWithSkillBindings"))                try App.enqueuePromptWithSkillBindings(app, prompt, skill_tokens)            else                try App.enqueuePrompt(app, prompt);            if (!accepted) return false;            if (resume_review) {                if (comptime @hasField(App, "queued_prompt_review")) {                    queue_rt.resumeAfterNewPrompt(app);                }            }            beginIdleSubmittedPromptTransition(app);            return true;        }         fn beginIdleSubmittedPromptTransition(app: *App) void {

Lines 1–500 of 1510.