From cfb845f1dd3f4edfef1378b8764f49475a6b31f7 Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Sun, 31 May 2026 22:18:12 -0500 Subject: [PATCH] add git comment --- src/api.ts | 37 +++++++++++++ src/index.ts | 3 ++ src/tui.ts | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 10 +++- 4 files changed, 194 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index 116164e..ea01723 100644 --- a/src/api.ts +++ b/src/api.ts @@ -295,3 +295,40 @@ export async function createIssue( throw new Error(`Network Error: ${error.message}`); } } + +/** + * Creates a comment on a specific issue. + */ +export async function createIssueComment( + config: Config, + issueNumber: number, + body: string +): Promise { + const client = createAxiosInstance(config); + try { + const response = await client.post(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`, { + body, + }); + + const item = response.data; + return { + id: item.id, + user: { + id: item.user?.id || 0, + login: item.user?.login || 'unknown', + full_name: item.user?.full_name || '', + }, + body: item.body, + created_at: item.created_at, + updated_at: item.updated_at, + }; + } catch (error: any) { + if (error.response) { + if (error.response.status === 401) { + throw new Error('Unauthorized: You must be logged in with a token to comment.'); + } + throw new Error(`Failed to create comment: ${error.response.data?.message || error.message}`); + } + throw new Error(`Network Error: ${error.message}`); + } +} diff --git a/src/index.ts b/src/index.ts index 1e72a37..9bbf445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,6 +73,9 @@ async function bootstrap() { body: '', activeField: 'title', }, + addCommentForm: { + body: '', + }, }; // If parameters are provided, try direct connection first diff --git a/src/tui.ts b/src/tui.ts index a736a13..c16b995 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,7 +1,7 @@ import readline from 'readline'; import chalk from 'chalk'; import { AppState, Issue, Comment } from './types.js'; -import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue } from './api.js'; +import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment } from './api.js'; import { saveGlobalConfig } from './config.js'; // Setup readline for stdin keypress events @@ -248,6 +248,8 @@ export class TuiEngine { this.handleDetailsKeypress(str, key); } else if (this.state.screen === 'create-issue') { this.handleCreateIssueKeypress(str, key); + } else if (this.state.screen === 'add-comment') { + this.handleAddCommentKeypress(str, key); } } @@ -666,6 +668,14 @@ export class TuiEngine { this.render(); return; } + + if (str === 'c' || str === 'C') { + this.state.screen = 'add-comment'; + this.state.addCommentForm.body = ''; + this.state.error = null; + this.render(); + return; + } if ((key && (key.name === 'escape' || key.name === 'backspace')) || str === '\u001b' || str === 'q' || str === 'Q') { this.state.screen = 'list'; @@ -695,6 +705,8 @@ export class TuiEngine { this.renderDetailsScreen(cols, rows); } else if (this.state.screen === 'create-issue') { this.renderCreateIssueScreen(cols, rows); + } else if (this.state.screen === 'add-comment') { + this.renderAddCommentScreen(cols, rows); } } @@ -1096,7 +1108,7 @@ export class TuiEngine { scrollHelp += chalk.yellow(` (${pct}%)`); } - const helpLine = chalk.gray(` [Esc/Backspace] Back to List [O] Settings ${scrollHelp}`); + const helpLine = chalk.gray(` [Esc/Backspace] Back to List [C] Add Comment [O] Settings ${scrollHelp}`); process.stdout.write(helpLine); } @@ -1344,5 +1356,136 @@ export class TuiEngine { this.render(); } } + + private renderAddCommentScreen(cols: number, rows: number) { + const borderCh = chalk.bold.hex('#4A90E2')('│'); + + // Header + const title = ' Add Comment '; + const leftLen = stripAnsi(title).length; + let leftHeader = chalk.bold.hex('#4A90E2')(title); + + console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); + + if (this.state.error) { + console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); + console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + } + + const form = this.state.addCommentForm; + + // Body Field + const bodyLabel = chalk.yellow.bold(' Comment (markdown supported): '); + console.log(borderCh + bodyLabel + ' '.repeat(cols - stripAnsi(bodyLabel).length - 2) + borderCh); + console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + + // Body Content (multiline) + const errorOffset = this.state.error ? 2 : 0; + const bodyRows = rows - 8 - errorOffset; // available space for body + const lines = form.body.split('\n'); + + // Pagination for body if it gets too long + const startLine = Math.max(0, lines.length - bodyRows); + + for (let i = 0; i < bodyRows; i++) { + const lineIdx = startLine + i; + let lineContent = ''; + if (lineIdx < lines.length) { + lineContent = lines[lineIdx]; + } + + let visLine = lineContent; + if (visLine.length > cols - 5) { + visLine = visLine.substring(visLine.length - (cols - 5)); + } + + if (lineIdx === lines.length - 1) { + visLine = visLine + chalk.inverse(' ') + ' '.repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); + } else { + visLine = visLine.padEnd(cols - 4); + } + + console.log(borderCh + ' ' + visLine + ' ' + borderCh); + } + + console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + + // Help line + const helpLine = chalk.gray(' [Ctrl+S] Submit [Esc] Cancel'); + const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; + const issueLen = stripAnsi(issueStr).length; + + const maxHelpLen = cols - issueLen - 1; + let helpLinePlain = helpLine; + if (stripAnsi(helpLinePlain).length > maxHelpLen) { + helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + } + + const spaces = cols - stripAnsi(helpLinePlain).length - issueLen - 1; + process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + issueStr); + } + + private async handleAddCommentKeypress(str: string, key: any) { + if (key && key.name === 'escape') { + this.state.screen = 'details'; + this.state.error = null; + this.render(); + return; + } + + if (key && key.ctrl && key.name === 's') { + // Submit comment + if (!this.state.addCommentForm.body.trim()) { + this.state.error = 'Comment cannot be empty.'; + this.render(); + return; + } + + if (!this.state.selectedIssue) { + this.state.error = 'No issue selected.'; + this.render(); + return; + } + + this.state.error = null; + this.state.loading = true; + this.render(); + try { + await createIssueComment(this.state.config, this.state.selectedIssue.number, this.state.addCommentForm.body); + this.state.screen = 'details'; + this.state.loading = false; + // Refresh comments + this.loadComments(this.state.selectedIssue); + } catch (err: any) { + this.state.error = err.message; + this.state.loading = false; + this.render(); + } + return; + } + + let val = this.state.addCommentForm.body; + + if (key && key.name === 'backspace') { + if (val.length > 0) { + this.state.addCommentForm.body = val.slice(0, -1); + this.render(); + } + return; + } + + if (key && key.name === 'return') { + this.state.addCommentForm.body += '\n'; + this.render(); + return; + } + + if (str && str.length === 1 && !key.ctrl && !key.meta) { + this.state.error = null; + this.state.addCommentForm.body += str; + this.render(); + } + } } diff --git a/src/types.ts b/src/types.ts index 5f6886d..5cad291 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,7 +54,12 @@ export interface CreateIssueForm { activeField: 'title' | 'body'; } -export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue'; +export interface AddCommentForm { + body: string; +} + +export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment'; + export interface RepoItem { id: number; @@ -99,5 +104,8 @@ export interface AppState { // Create Issue Form State createIssueForm: CreateIssueForm; + + // Add Comment Form State + addCommentForm: AddCommentForm; }