Skip to content

Commit 0b6dabb

Browse files
committed
feat: Add automatic YouTube URL attachment and plain text export
This commit introduces two new features to improve user workflow and the utility of history exports. - **Automatic YouTube URL Handling:** Users can now paste YouTube URLs directly into their prompts. The CLI will automatically detect these URLs, attach them as video context for the model, and then strip them from the text prompt itself. This allows for a more seamless experience when asking questions about video content. - **Plain Text in Markdown Exports:** The `export_history_to_markdown` function has been enhanced. When exporting a session that includes pasted text attachments (mime type `text/plain`), the command now decodes the Base64 data and embeds the plain text directly into the Markdown file within a code block. This makes the exported history more readable and self-contained. To support these changes, a `base64_decode` function and a `process_and_strip_urls` helper have been added.
1 parent e9aa8f0 commit 0b6dabb

2 files changed

Lines changed: 164 additions & 4 deletions

File tree

Changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
### **Version 2.1.0**
2+
3+
This is a usability and feature enhancement release focused on streamlining the workflow for YouTube video analysis and improving the utility of conversation exports.
4+
5+
* **Features:**
6+
* **Automatic YouTube URL Handling:** In interactive mode, you can now paste a YouTube URL directly into the chat. The client will automatically detect the URL, attach it as context for the next prompt, and remove it from the input text. This eliminates the need to attach URLs as separate command-line arguments.
7+
* **Enhanced Markdown Export:** The `/export` command has been improved. It now decodes and embeds the content of any `text/plain` file attachments directly into the exported Markdown file. This is especially useful for preserving text that was pasted or piped into a session, making the exported history more complete and readable.
8+
* **Improvements:**
9+
* A new `base64_decode` utility function was added to support the improved export functionality.
10+
111
### **Version 2.0.9**
212

313
This is a user-safety and content filtering release that gives users explicit control over the API's safety settings.

gemini-cli.c

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
// --- Data Structures ---
6565
typedef struct { unsigned char* data; size_t size; } GzipResult;
66+
typedef struct { unsigned char* data; size_t size; } Base64DecodeResult;
6667
typedef enum { PART_TYPE_TEXT, PART_TYPE_FILE, PART_TYPE_URI } PartType;
6768
typedef struct {
6869
PartType type;
@@ -121,6 +122,7 @@ void free_history(History* history);
121122
void free_content(Content* content);
122123
int get_token_count(AppState* state);
123124
char* base64_encode(const unsigned char* data, size_t input_length);
125+
Base64DecodeResult base64_decode(const char* data);
124126
const char* get_mime_type(const char* filename);
125127
GzipResult gzip_compress(const unsigned char* input_data, size_t input_size);
126128
cJSON* build_request_json(AppState* state);
@@ -158,6 +160,8 @@ static void process_free_line(char* line, AppState* state);
158160
static size_t write_free_memory_callback(void* contents, size_t size, size_t nmemb, void* userp);
159161
char* build_free_request_payload(AppState* state, const char* current_prompt, bool is_pro_model);
160162

163+
char* process_and_strip_urls(const char* original_prompt, AppState* state);
164+
161165
/**
162166
* @brief Parses a single line from the API's streaming response.
163167
* @details This function is designed to handle a Server-Sent Event (SSE)
@@ -595,6 +599,22 @@ void generate_session(int argc, char* argv[], bool interactive, bool is_stdin_a_
595599
char* p = line;
596600
while(isspace((unsigned char)*p)) p++;
597601

602+
if (p[0] != '/') {
603+
// Create a new string with URLs stripped out and attached.
604+
char* stripped_line = process_and_strip_urls(p, &state);
605+
606+
if (stripped_line) {
607+
// Free the original line from readline().
608+
free(line);
609+
// Point our main 'line' pointer to the new, cleaned string.
610+
line = stripped_line;
611+
// Reset 'p' to point to the start of our new line.
612+
p = line;
613+
// Re-trim leading whitespace, in case stripping the URL left some behind.
614+
while(isspace((unsigned char)*p)) p++;
615+
}
616+
}
617+
598618
// Add non-empty lines to the readline history.
599619
if (*p) {
600620
#ifndef _WIN32
@@ -2300,6 +2320,7 @@ void list_available_models(AppState* state) {
23002320
* the AppState. It formats each user and model turn into a simple Markdown
23012321
* structure, including the system prompt, text content, and placeholders
23022322
* for file attachments. The output is saved to the specified file path.
2323+
* MODIFIED: It now decodes and prints the content of text/plain attachments.
23032324
* @param state The current application state, containing the history to be exported.
23042325
* @param filepath The path to the Markdown file that will be created or overwritten.
23052326
*/
@@ -2339,10 +2360,26 @@ void export_history_to_markdown(AppState* state, const char* filepath) {
23392360
fprintf(file, "%s\n", part->text);
23402361
has_text = true;
23412362
} else if (part->type == PART_TYPE_FILE) {
2342-
// For file attachments, write a placeholder indicating the file's name and type.
2343-
const char* filename = part->filename ? part->filename : "Pasted Data";
2344-
const char* mime_type = part->mime_type ? part->mime_type : "unknown";
2345-
fprintf(file, "\n`[Attached File: %s (%s)]`\n", filename, mime_type);
2363+
// --- MODIFICATION START ---
2364+
// Check if the attachment is text/plain and has data to be decoded.
2365+
if (part->mime_type && strcmp(part->mime_type, "text/plain") == 0 && part->base64_data) {
2366+
Base64DecodeResult decoded = base64_decode(part->base64_data);
2367+
if (decoded.data) {
2368+
// If decoding is successful, print the content in a formatted block.
2369+
fprintf(file, "\n```text\n%s\n```\n", (char*)decoded.data);
2370+
free(decoded.data); // Free the memory from the decoder.
2371+
} else {
2372+
// If decoding fails, print a placeholder with an error.
2373+
const char* filename = part->filename ? part->filename : "Pasted Data";
2374+
fprintf(file, "\n`[Attached File: %s (text/plain, decoding failed)]`\n", filename);
2375+
}
2376+
} else {
2377+
// For all other file types, use the original placeholder.
2378+
const char* filename = part->filename ? part->filename : "Pasted Data";
2379+
const char* mime_type = part->mime_type ? part->mime_type : "unknown";
2380+
fprintf(file, "\n`[Attached File: %s (%s)]`\n", filename, mime_type);
2381+
}
2382+
// --- MODIFICATION END ---
23462383
}
23472384
}
23482385

@@ -4225,6 +4262,71 @@ bool is_youtube_url(const char* url) {
42254262
return (strstr(url, "youtube.com/watch") != NULL || strstr(url, "youtu.be/") != NULL);
42264263
}
42274264

4265+
/**
4266+
* @brief Scans a prompt for YouTube URLs, attaches them, and returns a new prompt string.
4267+
* @details This function takes a string, creates a modifiable copy, and iterates
4268+
* through it to find and process YouTube URLs. For each URL found, it
4269+
* uses the existing attachment handler and then removes the URL from the
4270+
* string. This function correctly handles multiple URLs in a single prompt.
4271+
* @param original_prompt The user's raw input string.
4272+
* @param state A pointer to the application state to add attachments to.
4273+
* @return A new, dynamically allocated string containing the prompt with all
4274+
* YouTube URLs stripped out. The caller is responsible for freeing this memory.
4275+
* This new string is a distinct allocation from the original_prompt.
4276+
*/
4277+
char* process_and_strip_urls(const char* original_prompt, AppState* state) {
4278+
// Start with a mutable copy of the original prompt.
4279+
char* processed_prompt = strdup(original_prompt);
4280+
if (!processed_prompt) return NULL; // Should not happen
4281+
4282+
bool url_was_found_and_removed;
4283+
do {
4284+
url_was_found_and_removed = false;
4285+
char* search_start = processed_prompt;
4286+
4287+
// Find the beginning of a URL.
4288+
char* url_start = strstr(search_start, "https://");
4289+
if (!url_start) {
4290+
url_start = strstr(search_start, "http://");
4291+
}
4292+
4293+
if (url_start) {
4294+
// Find the end of the URL (a space or the end of the string).
4295+
char* url_end = url_start;
4296+
while (*url_end != '\0' && !isspace((unsigned char)*url_end)) {
4297+
url_end++;
4298+
}
4299+
4300+
// Temporarily null-terminate the URL to treat it as a separate string.
4301+
char original_char = *url_end;
4302+
*url_end = '\0';
4303+
4304+
// Check if this substring is a valid YouTube URL.
4305+
if (is_youtube_url(url_start)) {
4306+
// Attach the URL using the existing handler.
4307+
handle_attachment_from_stream(NULL, url_start, "video/*", state);
4308+
4309+
// Use the existing str_replace helper to remove the URL.
4310+
// We pass an empty string "" as the replacement.
4311+
char* temp_prompt = str_replace(processed_prompt, url_start, "");
4312+
4313+
// Free the old processed_prompt and point to the new one.
4314+
free(processed_prompt);
4315+
processed_prompt = temp_prompt;
4316+
4317+
// Set flag to true so the `do-while` loop runs again,
4318+
// allowing us to find multiple URLs in the same prompt.
4319+
url_was_found_and_removed = true;
4320+
}
4321+
4322+
// Restore the original character if we modified the string.
4323+
*url_end = original_char;
4324+
}
4325+
} while (url_was_found_and_removed);
4326+
4327+
return processed_prompt;
4328+
}
4329+
42284330
/**
42294331
* @brief Reads data from a stream and creates a pending file attachment.
42304332
* @details This function is a robust, production-ready handler for all file and
@@ -4478,6 +4580,54 @@ char* base64_encode(const unsigned char* data, size_t input_length) {
44784580
return encoded_data;
44794581
}
44804582

4583+
/**
4584+
* @brief Decodes a Base64 string into binary data.
4585+
* @param data The null-terminated Base64 string to decode.
4586+
* @return A Base64DecodeResult struct. The `data` field contains the decoded
4587+
* bytes and `size` holds the length. The caller is responsible for
4588+
* freeing the `data` buffer. `data` will be NULL on failure.
4589+
*/
4590+
Base64DecodeResult base64_decode(const char* data) {
4591+
static const int b64_inv_table[] = {
4592+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
4593+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
4594+
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
4595+
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
4596+
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
4597+
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
4598+
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
4599+
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
4600+
};
4601+
4602+
Base64DecodeResult result = { .data = NULL, .size = 0 };
4603+
size_t input_len = strlen(data);
4604+
4605+
if (input_len % 4 != 0) return result; // Invalid Base64 length
4606+
4607+
result.size = input_len / 4 * 3;
4608+
if (data[input_len - 1] == '=') (result.size)--;
4609+
if (data[input_len - 2] == '=') (result.size)--;
4610+
4611+
result.data = malloc(result.size + 1);
4612+
if (!result.data) return result;
4613+
4614+
for (size_t i = 0, j = 0; i < input_len; ) {
4615+
uint32_t sextet_a = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0;
4616+
uint32_t sextet_b = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0;
4617+
uint32_t sextet_c = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0;
4618+
uint32_t sextet_d = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0;
4619+
4620+
uint32_t triple = (sextet_a << 18) + (sextet_b << 12) + (sextet_c << 6) + sextet_d;
4621+
4622+
if (j < result.size) result.data[j++] = (triple >> 16) & 0xFF;
4623+
if (j < result.size) result.data[j++] = (triple >> 8) & 0xFF;
4624+
if (j < result.size) result.data[j++] = triple & 0xFF;
4625+
}
4626+
4627+
result.data[result.size] = '\0';
4628+
return result;
4629+
}
4630+
44814631
/**
44824632
* @brief Performs the low-level cURL request for the official Gemini API.
44834633
* @details This is the core transport function for all POST requests to the

0 commit comments

Comments
 (0)