clairo 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1201 -786
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import meow from "meow";
5
5
 
6
6
  // src/app.tsx
7
- import { useState as useState8 } from "react";
8
- import { Box as Box13, useApp, useInput as useInput10 } from "ink";
7
+ import { useCallback as useCallback4, useState as useState9 } from "react";
8
+ import { Box as Box16, useApp, useInput as useInput13 } from "ink";
9
9
 
10
10
  // src/components/github/GitHubView.tsx
11
11
  import { exec as exec3 } from "child_process";
12
- import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
12
+ import { useCallback, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
13
13
  import { TitledBox as TitledBox4 } from "@mishieck/ink-titled-box";
14
14
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
15
15
 
@@ -241,803 +241,966 @@ async function getPRDetails(prNumber, repo) {
241
241
  }
242
242
  }
243
243
 
244
- // src/components/github/PRDetailsBox.tsx
245
- import { useRef } from "react";
246
- import open from "open";
247
- import { TitledBox } from "@mishieck/ink-titled-box";
248
- import { Box as Box2, Text as Text2, useInput } from "ink";
249
- import { ScrollView } from "ink-scroll-view";
250
-
251
- // src/components/ui/Markdown.tsx
252
- import { Box, Text } from "ink";
253
- import Link from "ink-link";
254
- import { marked } from "marked";
255
- import Table from "cli-table3";
256
- import { jsx, jsxs } from "react/jsx-runtime";
257
- function Markdown({ children }) {
258
- const tokens = marked.lexer(children);
259
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
244
+ // src/lib/jira/parser.ts
245
+ var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
246
+ function isValidTicketKeyFormat(key) {
247
+ return TICKET_KEY_PATTERN.test(key.toUpperCase());
260
248
  }
261
- function TokenRenderer({ token }) {
262
- var _a, _b;
263
- switch (token.type) {
264
- case "heading":
265
- return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
266
- case "paragraph": {
267
- const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
268
- var _a2;
269
- return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
270
- });
271
- if (hasLinks) {
272
- return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
273
- }
274
- return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
275
- }
276
- case "code":
277
- return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
278
- case "blockquote":
279
- return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
280
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
281
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
282
- ] });
283
- case "list":
284
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
285
- /* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
286
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
287
- ] }, idx)) });
288
- case "table":
289
- return /* @__PURE__ */ jsx(TableRenderer, { token });
290
- case "hr":
291
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
292
- case "space":
293
- return null;
294
- default:
295
- if ("text" in token && typeof token.text === "string") {
296
- return /* @__PURE__ */ jsx(Text, { children: token.text });
297
- }
298
- return null;
249
+ function parseTicketKey(input) {
250
+ const trimmed = input.trim();
251
+ const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
252
+ if (urlMatch) {
253
+ return urlMatch[1].toUpperCase();
254
+ }
255
+ const upperInput = trimmed.toUpperCase();
256
+ if (isValidTicketKeyFormat(upperInput)) {
257
+ return upperInput;
299
258
  }
259
+ return null;
300
260
  }
301
- function TableRenderer({ token }) {
302
- const table = new Table({
303
- head: token.header.map((cell) => renderInlineToString(cell.tokens)),
304
- style: { head: ["cyan"], border: ["gray"] }
305
- });
306
- for (const row of token.rows) {
307
- table.push(row.map((cell) => renderInlineToString(cell.tokens)));
261
+ function extractTicketKeyFromBranch(branchName) {
262
+ const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
263
+ if (match) {
264
+ const candidate = match[1].toUpperCase();
265
+ if (isValidTicketKeyFormat(candidate)) {
266
+ return candidate;
267
+ }
308
268
  }
309
- return /* @__PURE__ */ jsx(Text, { children: table.toString() });
269
+ return null;
310
270
  }
311
- function renderInline(tokens) {
312
- if (!tokens) return null;
313
- return tokens.map((token, idx) => {
314
- switch (token.type) {
315
- case "text":
316
- return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
317
- case "strong":
318
- return /* @__PURE__ */ jsx(Text, { bold: true, children: renderInline(token.tokens) }, idx);
319
- case "em":
320
- return /* @__PURE__ */ jsx(Text, { italic: true, children: renderInline(token.tokens) }, idx);
321
- case "codespan":
322
- return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
323
- "`",
324
- token.text,
325
- "`"
326
- ] }, idx);
327
- case "link":
328
- return /* @__PURE__ */ jsx(Link, { url: token.href, children: /* @__PURE__ */ jsx(Text, { color: "blue", children: renderInlineToString(token.tokens) }) }, idx);
329
- case "image":
330
- return /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
331
- "[Image: ",
332
- token.text || token.href,
333
- "]"
334
- ] }, idx);
335
- case "br":
336
- return /* @__PURE__ */ jsx(Text, { children: "\n" }, idx);
337
- case "del":
338
- return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: renderInline(token.tokens) }, idx);
339
- default:
340
- if ("text" in token && typeof token.text === "string") {
341
- return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
342
- }
343
- return null;
271
+
272
+ // src/lib/jira/config.ts
273
+ function isJiraConfigured(repoPath) {
274
+ const config = getRepoConfig(repoPath);
275
+ return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
276
+ }
277
+ function getJiraSiteUrl(repoPath) {
278
+ const config = getRepoConfig(repoPath);
279
+ return config.jiraSiteUrl ?? null;
280
+ }
281
+ function setJiraSiteUrl(repoPath, siteUrl) {
282
+ updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
283
+ }
284
+ function getJiraCredentials(repoPath) {
285
+ const config = getRepoConfig(repoPath);
286
+ return {
287
+ email: config.jiraEmail ?? null,
288
+ apiToken: config.jiraApiToken ?? null
289
+ };
290
+ }
291
+ function setJiraCredentials(repoPath, email, apiToken) {
292
+ updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
293
+ }
294
+ function getLinkedTickets(repoPath, branch) {
295
+ var _a;
296
+ const config = getRepoConfig(repoPath);
297
+ return ((_a = config.branchTickets) == null ? void 0 : _a[branch]) ?? [];
298
+ }
299
+ function addLinkedTicket(repoPath, branch, ticket) {
300
+ const config = getRepoConfig(repoPath);
301
+ const branchTickets = config.branchTickets ?? {};
302
+ const tickets = branchTickets[branch] ?? [];
303
+ if (tickets.some((t) => t.key === ticket.key)) {
304
+ return;
305
+ }
306
+ updateRepoConfig(repoPath, {
307
+ branchTickets: {
308
+ ...branchTickets,
309
+ [branch]: [...tickets, ticket]
344
310
  }
345
311
  });
346
312
  }
347
- function renderInlineToString(tokens) {
348
- if (!tokens) return "";
349
- return tokens.map((token) => {
350
- if ("text" in token && typeof token.text === "string") {
351
- return token.text;
313
+ function removeLinkedTicket(repoPath, branch, ticketKey) {
314
+ const config = getRepoConfig(repoPath);
315
+ const branchTickets = config.branchTickets ?? {};
316
+ const tickets = branchTickets[branch] ?? [];
317
+ updateRepoConfig(repoPath, {
318
+ branchTickets: {
319
+ ...branchTickets,
320
+ [branch]: tickets.filter((t) => t.key !== ticketKey)
352
321
  }
353
- if ("tokens" in token && Array.isArray(token.tokens)) {
354
- return renderInlineToString(token.tokens);
322
+ });
323
+ }
324
+ function updateTicketStatus(repoPath, branch, ticketKey, newStatus) {
325
+ const config = getRepoConfig(repoPath);
326
+ const branchTickets = config.branchTickets ?? {};
327
+ const tickets = branchTickets[branch] ?? [];
328
+ updateRepoConfig(repoPath, {
329
+ branchTickets: {
330
+ ...branchTickets,
331
+ [branch]: tickets.map((t) => t.key === ticketKey ? { ...t, status: newStatus } : t)
355
332
  }
356
- return "";
357
- }).join("");
333
+ });
358
334
  }
359
335
 
360
- // src/components/github/PRDetailsBox.tsx
361
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
362
- function getCheckColor(check) {
363
- const conclusion = check.conclusion ?? check.state;
364
- if (conclusion === "SUCCESS") return "green";
365
- if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
366
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
367
- if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
368
- return "yellow";
369
- if (check.status === "COMPLETED") return "green";
370
- return void 0;
336
+ // src/lib/jira/api.ts
337
+ function createAuthHeader(email, apiToken) {
338
+ const credentials = Buffer.from(`${email}:${apiToken}`).toString("base64");
339
+ return `Basic ${credentials}`;
371
340
  }
372
- function getCheckIcon(check) {
373
- const conclusion = check.conclusion ?? check.state;
374
- if (conclusion === "SUCCESS") return "\u2713";
375
- if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
376
- if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
377
- if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
378
- return "\u25CF";
379
- if (check.status === "COMPLETED") return "\u2713";
380
- return "?";
341
+ async function jiraFetch(auth, endpoint, options) {
342
+ const url = `${auth.siteUrl}/rest/api/3${endpoint}`;
343
+ const method = (options == null ? void 0 : options.method) ?? "GET";
344
+ try {
345
+ const headers = {
346
+ Authorization: createAuthHeader(auth.email, auth.apiToken),
347
+ Accept: "application/json"
348
+ };
349
+ const fetchOptions = { method, headers };
350
+ if (options == null ? void 0 : options.body) {
351
+ headers["Content-Type"] = "application/json";
352
+ fetchOptions.body = JSON.stringify(options.body);
353
+ }
354
+ const response = await fetch(url, fetchOptions);
355
+ if (!response.ok) {
356
+ const text = await response.text();
357
+ return { ok: false, status: response.status, error: text };
358
+ }
359
+ if (response.status === 204) {
360
+ return { ok: true, status: response.status, data: null };
361
+ }
362
+ const data = await response.json();
363
+ return { ok: true, status: response.status, data };
364
+ } catch (err) {
365
+ const message = err instanceof Error ? err.message : "Network error";
366
+ return { ok: false, status: 0, error: message };
367
+ }
381
368
  }
382
- function PRDetailsBox({ pr, loading, error, isFocused }) {
383
- var _a, _b, _c, _d, _e, _f, _g;
384
- const scrollRef = useRef(null);
385
- const title = "[3] PR Details";
386
- const borderColor = isFocused ? "yellow" : void 0;
387
- const displayTitle = pr ? `${title} - #${pr.number}` : title;
388
- const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
389
- const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
390
- const getMergeDisplay = () => {
391
- if (!pr) return { text: "UNKNOWN", color: "yellow" };
392
- if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
393
- if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
394
- if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
395
- if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
396
- return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
397
- };
398
- const mergeDisplay = getMergeDisplay();
399
- useInput(
400
- (input, key) => {
401
- var _a2, _b2;
402
- if (key.upArrow || input === "k") {
403
- (_a2 = scrollRef.current) == null ? void 0 : _a2.scrollBy(-1);
404
- }
405
- if (key.downArrow || input === "j") {
406
- (_b2 = scrollRef.current) == null ? void 0 : _b2.scrollBy(1);
407
- }
408
- if (input === "o" && (pr == null ? void 0 : pr.url)) {
409
- open(pr.url).catch(() => {
410
- });
411
- }
412
- },
413
- { isActive: isFocused }
414
- );
415
- return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx2(ScrollView, { ref: scrollRef, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
416
- loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
417
- error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
418
- !loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
419
- !loading && !error && pr && /* @__PURE__ */ jsxs2(Fragment, { children: [
420
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: pr.title }),
421
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
422
- "by ",
423
- ((_a = pr.author) == null ? void 0 : _a.login) ?? "unknown",
424
- " | ",
425
- ((_b = pr.commits) == null ? void 0 : _b.length) ?? 0,
426
- " commits"
427
- ] }),
428
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
429
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
430
- /* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
431
- /* @__PURE__ */ jsx2(Text2, { children: " | " }),
432
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
433
- /* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
434
- ] }),
435
- (((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
436
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Assignees: " }),
437
- /* @__PURE__ */ jsx2(Text2, { children: pr.assignees.map((a) => a.login).join(", ") })
438
- ] }),
439
- (((_d = pr.reviews) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
440
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reviews:" }),
441
- pr.reviews.map((review, idx) => {
442
- const color = review.state === "APPROVED" ? "green" : review.state === "CHANGES_REQUESTED" ? "red" : review.state === "COMMENTED" ? "blue" : "yellow";
443
- const icon = review.state === "APPROVED" ? "\u2713" : review.state === "CHANGES_REQUESTED" ? "\u2717" : review.state === "COMMENTED" ? "\u{1F4AC}" : "\u25CB";
444
- return /* @__PURE__ */ jsxs2(Text2, { color, children: [
445
- " ",
446
- icon,
447
- " ",
448
- review.author.login
449
- ] }, idx);
450
- })
451
- ] }),
452
- (((_e = pr.reviewRequests) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { children: [
453
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Pending: " }),
454
- /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
455
- ] }),
456
- (((_f = pr.statusCheckRollup) == null ? void 0 : _f.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
457
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Checks:" }),
458
- (_g = pr.statusCheckRollup) == null ? void 0 : _g.map((check, idx) => /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
459
- " ",
460
- getCheckIcon(check),
461
- " ",
462
- check.name ?? check.context
463
- ] }, idx))
464
- ] }),
465
- pr.body && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
466
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
467
- /* @__PURE__ */ jsx2(Markdown, { children: pr.body })
468
- ] })
469
- ] })
470
- ] }) }) }) });
369
+ async function validateCredentials(auth) {
370
+ const result = await jiraFetch(auth, "/myself");
371
+ if (!result.ok) {
372
+ if (result.status === 401 || result.status === 403) {
373
+ return {
374
+ success: false,
375
+ error: "Invalid credentials. Check your email and API token.",
376
+ errorType: "auth_error"
377
+ };
378
+ }
379
+ return {
380
+ success: false,
381
+ error: result.error ?? "Failed to connect to Jira",
382
+ errorType: "api_error"
383
+ };
384
+ }
385
+ return { success: true, data: result.data };
386
+ }
387
+ async function getIssue(auth, ticketKey) {
388
+ const result = await jiraFetch(auth, `/issue/${ticketKey}?fields=summary,status`);
389
+ if (!result.ok) {
390
+ if (result.status === 401 || result.status === 403) {
391
+ return {
392
+ success: false,
393
+ error: "Authentication failed",
394
+ errorType: "auth_error"
395
+ };
396
+ }
397
+ if (result.status === 404) {
398
+ return {
399
+ success: false,
400
+ error: `Ticket ${ticketKey} not found`,
401
+ errorType: "invalid_ticket"
402
+ };
403
+ }
404
+ return {
405
+ success: false,
406
+ error: result.error ?? "Failed to fetch issue",
407
+ errorType: "api_error"
408
+ };
409
+ }
410
+ return { success: true, data: result.data };
411
+ }
412
+ async function getTransitions(auth, ticketKey) {
413
+ const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`);
414
+ if (!result.ok) {
415
+ if (result.status === 401 || result.status === 403) {
416
+ return {
417
+ success: false,
418
+ error: "Authentication failed",
419
+ errorType: "auth_error"
420
+ };
421
+ }
422
+ if (result.status === 404) {
423
+ return {
424
+ success: false,
425
+ error: `Ticket ${ticketKey} not found`,
426
+ errorType: "invalid_ticket"
427
+ };
428
+ }
429
+ return {
430
+ success: false,
431
+ error: result.error ?? "Failed to fetch transitions",
432
+ errorType: "api_error"
433
+ };
434
+ }
435
+ const data = result.data;
436
+ return { success: true, data: data.transitions };
437
+ }
438
+ async function applyTransition(auth, ticketKey, transitionId) {
439
+ const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`, {
440
+ method: "POST",
441
+ body: { transition: { id: transitionId } }
442
+ });
443
+ if (!result.ok) {
444
+ if (result.status === 401 || result.status === 403) {
445
+ return {
446
+ success: false,
447
+ error: "Authentication failed",
448
+ errorType: "auth_error"
449
+ };
450
+ }
451
+ if (result.status === 404) {
452
+ return {
453
+ success: false,
454
+ error: `Ticket ${ticketKey} not found`,
455
+ errorType: "invalid_ticket"
456
+ };
457
+ }
458
+ return {
459
+ success: false,
460
+ error: result.error ?? "Failed to apply transition",
461
+ errorType: "api_error"
462
+ };
463
+ }
464
+ return { success: true, data: null };
471
465
  }
472
466
 
473
- // src/components/github/PullRequestsBox.tsx
474
- import { useEffect, useState } from "react";
475
- import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
476
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
477
-
478
- // src/lib/clipboard.ts
479
- import { exec as exec2 } from "child_process";
480
- async function copyToClipboard(text) {
481
- var _a, _b;
482
- const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
467
+ // src/lib/logs/index.ts
468
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, appendFileSync, writeFileSync as writeFileSync2 } from "fs";
469
+ import { homedir as homedir2 } from "os";
470
+ import { join as join2 } from "path";
471
+ import { spawnSync } from "child_process";
472
+ var LOGS_DIRECTORY = join2(homedir2(), ".clairo", "logs");
473
+ function ensureLogsDirectory() {
474
+ if (!existsSync2(LOGS_DIRECTORY)) {
475
+ mkdirSync2(LOGS_DIRECTORY, { recursive: true });
476
+ }
477
+ }
478
+ function getTodayDate() {
479
+ const now = /* @__PURE__ */ new Date();
480
+ const year = now.getFullYear();
481
+ const month = String(now.getMonth() + 1).padStart(2, "0");
482
+ const day = String(now.getDate()).padStart(2, "0");
483
+ return `${year}-${month}-${day}`;
484
+ }
485
+ function formatTimestamp() {
486
+ const now = /* @__PURE__ */ new Date();
487
+ const hours = String(now.getHours()).padStart(2, "0");
488
+ const minutes = String(now.getMinutes()).padStart(2, "0");
489
+ return `${hours}:${minutes}`;
490
+ }
491
+ function listLogFiles() {
492
+ ensureLogsDirectory();
483
493
  try {
484
- const child = exec2(command);
485
- (_a = child.stdin) == null ? void 0 : _a.write(text);
486
- (_b = child.stdin) == null ? void 0 : _b.end();
487
- await new Promise((resolve, reject) => {
488
- child.on("close", (code) => {
489
- if (code === 0) resolve();
490
- else reject(new Error(`Clipboard command exited with code ${code}`));
491
- });
492
- });
493
- return true;
494
+ const files = readdirSync(LOGS_DIRECTORY);
495
+ const today = getTodayDate();
496
+ const logFiles = files.filter((file) => /^\d{4}-\d{2}-\d{2}\.md$/.test(file)).map((file) => {
497
+ const date = file.replace(".md", "");
498
+ return {
499
+ date,
500
+ filename: file,
501
+ isToday: date === today
502
+ };
503
+ }).sort((a, b) => b.date.localeCompare(a.date));
504
+ return logFiles;
494
505
  } catch {
506
+ return [];
507
+ }
508
+ }
509
+ function readLog(date) {
510
+ const filePath = join2(LOGS_DIRECTORY, `${date}.md`);
511
+ try {
512
+ if (!existsSync2(filePath)) {
513
+ return null;
514
+ }
515
+ return readFileSync2(filePath, "utf-8");
516
+ } catch {
517
+ return null;
518
+ }
519
+ }
520
+ function getLogFilePath(date) {
521
+ return join2(LOGS_DIRECTORY, `${date}.md`);
522
+ }
523
+ function logExists(date) {
524
+ return existsSync2(getLogFilePath(date));
525
+ }
526
+ function createEmptyLog(date) {
527
+ ensureLogsDirectory();
528
+ const filePath = getLogFilePath(date);
529
+ if (existsSync2(filePath)) {
530
+ return;
531
+ }
532
+ const header = `# Log - ${date}
533
+ `;
534
+ writeFileSync2(filePath, header);
535
+ }
536
+ function appendToLog(date, entry) {
537
+ ensureLogsDirectory();
538
+ const filePath = getLogFilePath(date);
539
+ if (!existsSync2(filePath)) {
540
+ const header = `# Log - ${date}
541
+
542
+ `;
543
+ writeFileSync2(filePath, header);
544
+ }
545
+ appendFileSync(filePath, entry);
546
+ }
547
+ function openLogInEditor(date) {
548
+ const filePath = getLogFilePath(date);
549
+ if (!existsSync2(filePath)) {
495
550
  return false;
496
551
  }
552
+ const editor = process.env.VISUAL || process.env.EDITOR || "vi";
553
+ const result = spawnSync(editor, [filePath], {
554
+ stdio: "inherit"
555
+ });
556
+ process.stdout.write("\x1B[2J\x1B[H");
557
+ process.stdout.emit("resize");
558
+ return result.status === 0;
497
559
  }
498
560
 
499
- // src/components/github/PullRequestsBox.tsx
500
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
501
- function PullRequestsBox({
502
- prs,
503
- selectedPR,
504
- onSelect,
505
- onCreatePR,
506
- loading,
507
- error,
508
- branch,
509
- repoSlug,
510
- isFocused
511
- }) {
512
- const [highlightedIndex, setHighlightedIndex] = useState(0);
513
- const totalItems = prs.length + 1;
514
- useEffect(() => {
515
- const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
516
- if (idx >= 0) setHighlightedIndex(idx);
517
- }, [selectedPR, prs]);
518
- useInput2(
519
- (input, key) => {
520
- if (!isFocused) return;
521
- if (key.upArrow || input === "k") {
522
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
523
- }
524
- if (key.downArrow || input === "j") {
525
- setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
526
- }
527
- if (key.return) {
528
- if (highlightedIndex === prs.length) {
529
- onCreatePR();
530
- } else if (prs[highlightedIndex]) {
531
- onSelect(prs[highlightedIndex]);
532
- }
533
- }
534
- if (input === "y" && repoSlug && prs[highlightedIndex]) {
535
- const pr = prs[highlightedIndex];
536
- const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
537
- copyToClipboard(url);
538
- }
539
- },
540
- { isActive: isFocused }
541
- );
542
- const title = "[2] Pull Requests";
543
- const subtitle = branch ? ` (${branch})` : "";
544
- const borderColor = isFocused ? "yellow" : void 0;
545
- return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
546
- loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
547
- error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
548
- !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
549
- prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
550
- prs.map((pr, idx) => {
551
- const isHighlighted = isFocused && idx === highlightedIndex;
552
- const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
553
- const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
554
- return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
555
- prefix,
556
- "#",
557
- pr.number,
558
- " ",
559
- pr.isDraft ? "[Draft] " : "",
560
- pr.title
561
- ] }, pr.number);
562
- }),
563
- /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
564
- isFocused && highlightedIndex === prs.length ? "> " : " ",
565
- "+ Create new PR"
566
- ] })
567
- ] })
568
- ] }) });
561
+ // src/lib/logs/logger.ts
562
+ function logPRCreated(prNumber, title, jiraTickets) {
563
+ const timestamp = formatTimestamp();
564
+ const today = getTodayDate();
565
+ let entry = `## ${timestamp} - Created PR #${prNumber}
566
+
567
+ ${title}
568
+ `;
569
+ if (jiraTickets.length > 0) {
570
+ entry += `Jira: ${jiraTickets.join(", ")}
571
+ `;
572
+ }
573
+ entry += "\n";
574
+ appendToLog(today, entry);
569
575
  }
576
+ function logJiraStatusChanged(ticketKey, ticketName, oldStatus, newStatus) {
577
+ const timestamp = formatTimestamp();
578
+ const today = getTodayDate();
579
+ const entry = `## ${timestamp} - Updated Jira ticket
570
580
 
571
- // src/components/github/RemotesBox.tsx
572
- import { useEffect as useEffect2, useState as useState2 } from "react";
573
- import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
574
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
575
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
576
- function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
577
- const [highlightedIndex, setHighlightedIndex] = useState2(0);
578
- useEffect2(() => {
579
- const idx = remotes.findIndex((r) => r.name === selectedRemote);
580
- if (idx >= 0) setHighlightedIndex(idx);
581
- }, [selectedRemote, remotes]);
582
- useInput3(
583
- (input, key) => {
584
- if (!isFocused || remotes.length === 0) return;
585
- if (key.upArrow || input === "k") {
586
- setHighlightedIndex((prev) => Math.max(0, prev - 1));
587
- }
588
- if (key.downArrow || input === "j") {
589
- setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
590
- }
591
- if (key.return) {
592
- onSelect(remotes[highlightedIndex].name);
593
- }
594
- },
595
- { isActive: isFocused }
596
- );
597
- const title = "[1] Remotes";
598
- const borderColor = isFocused ? "yellow" : void 0;
599
- return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
600
- loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
601
- error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
602
- !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
603
- !loading && !error && remotes.map((remote, idx) => {
604
- const isHighlighted = isFocused && idx === highlightedIndex;
605
- const isSelected = remote.name === selectedRemote;
606
- const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
607
- return /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
608
- prefix,
609
- remote.name,
610
- " (",
611
- remote.url,
612
- ")"
613
- ] }, remote.name);
614
- })
615
- ] }) });
581
+ ${ticketKey}: ${ticketName}
582
+ ${oldStatus} \u2192 ${newStatus}
583
+
584
+ `;
585
+ appendToLog(today, entry);
616
586
  }
617
587
 
618
- // src/components/github/GitHubView.tsx
619
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
620
- function GitHubView({ isFocused, onKeybindingsChange }) {
621
- const [isRepo, setIsRepo] = useState3(null);
622
- const [repoPath, setRepoPath] = useState3(null);
623
- const [remotes, setRemotes] = useState3([]);
624
- const [currentBranch, setCurrentBranch] = useState3(null);
625
- const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
626
- const [selectedRemote, setSelectedRemote] = useState3(null);
627
- const [selectedPR, setSelectedPR] = useState3(null);
628
- const [prs, setPrs] = useState3([]);
629
- const [prDetails, setPrDetails] = useState3(null);
630
- const [loading, setLoading] = useState3({
631
- remotes: true,
632
- prs: false,
633
- details: false
634
- });
635
- const [errors, setErrors] = useState3({});
636
- const [focusedBox, setFocusedBox] = useState3("remotes");
637
- useEffect3(() => {
638
- if (!isFocused) {
639
- onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
640
- return;
641
- }
642
- const bindings = [];
643
- if (focusedBox === "remotes") {
644
- bindings.push({ key: "Enter", label: "Select Remote" });
645
- } else if (focusedBox === "prs") {
646
- bindings.push({ key: "n", label: "New PR", color: "green" });
647
- bindings.push({ key: "r", label: "Refresh" });
648
- bindings.push({ key: "o", label: "Open", color: "green" });
649
- bindings.push({ key: "y", label: "Copy Link" });
650
- } else if (focusedBox === "details") {
651
- bindings.push({ key: "r", label: "Refresh" });
652
- bindings.push({ key: "o", label: "Open", color: "green" });
653
- }
654
- onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
655
- }, [isFocused, focusedBox, onKeybindingsChange]);
656
- useEffect3(() => {
657
- const gitRepoCheck = isGitRepo();
658
- setIsRepo(gitRepoCheck);
659
- if (!gitRepoCheck) {
660
- setLoading((prev) => ({ ...prev, remotes: false }));
661
- setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
662
- return;
663
- }
664
- const rootResult = getRepoRoot();
665
- if (rootResult.success) {
666
- setRepoPath(rootResult.data);
667
- }
668
- const branchResult = getCurrentBranch();
669
- if (branchResult.success) {
670
- setCurrentBranch(branchResult.data);
671
- }
672
- const remotesResult = listRemotes();
673
- if (remotesResult.success) {
674
- setRemotes(remotesResult.data);
675
- const remoteNames = remotesResult.data.map((r) => r.name);
676
- const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
677
- setSelectedRemote(defaultRemote);
678
- } else {
679
- setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
680
- }
681
- setLoading((prev) => ({ ...prev, remotes: false }));
682
- }, []);
683
- const refreshPRs = useCallback(async () => {
684
- if (!currentBranch || !currentRepoSlug) return;
685
- setLoading((prev) => ({ ...prev, prs: true }));
686
- try {
687
- const result = await listPRsForBranch(currentBranch, currentRepoSlug);
688
- if (result.success) {
689
- setPrs(result.data);
690
- if (result.data.length > 0) {
691
- setSelectedPR((prev) => prev ?? result.data[0]);
692
- }
693
- setErrors((prev) => ({ ...prev, prs: void 0 }));
694
- } else {
695
- setErrors((prev) => ({ ...prev, prs: result.error }));
588
+ // src/components/github/PRDetailsBox.tsx
589
+ import { useRef } from "react";
590
+ import open from "open";
591
+ import { TitledBox } from "@mishieck/ink-titled-box";
592
+ import { Box as Box2, Text as Text2, useInput } from "ink";
593
+ import { ScrollView } from "ink-scroll-view";
594
+
595
+ // src/components/ui/Markdown.tsx
596
+ import { Box, Text } from "ink";
597
+ import Link from "ink-link";
598
+ import { marked } from "marked";
599
+ import Table from "cli-table3";
600
+ import { jsx, jsxs } from "react/jsx-runtime";
601
+ function Markdown({ children }) {
602
+ const tokens = marked.lexer(children);
603
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
604
+ }
605
+ function TokenRenderer({ token }) {
606
+ var _a, _b;
607
+ switch (token.type) {
608
+ case "heading":
609
+ return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
610
+ case "paragraph": {
611
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
612
+ var _a2;
613
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
614
+ });
615
+ if (hasLinks) {
616
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
696
617
  }
697
- } catch (err) {
698
- setErrors((prev) => ({ ...prev, prs: String(err) }));
699
- } finally {
700
- setLoading((prev) => ({ ...prev, prs: false }));
618
+ return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
701
619
  }
702
- }, [currentBranch, currentRepoSlug]);
703
- const refreshDetails = useCallback(async () => {
704
- if (!selectedPR || !currentRepoSlug) return;
705
- setLoading((prev) => ({ ...prev, details: true }));
706
- try {
707
- const result = await getPRDetails(selectedPR.number, currentRepoSlug);
708
- if (result.success) {
709
- setPrDetails(result.data);
710
- setErrors((prev) => ({ ...prev, details: void 0 }));
711
- } else {
712
- setErrors((prev) => ({ ...prev, details: result.error }));
620
+ case "code":
621
+ return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
622
+ case "blockquote":
623
+ return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
624
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
625
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
626
+ ] });
627
+ case "list":
628
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
629
+ /* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
630
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
631
+ ] }, idx)) });
632
+ case "table":
633
+ return /* @__PURE__ */ jsx(TableRenderer, { token });
634
+ case "hr":
635
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
636
+ case "space":
637
+ return null;
638
+ default:
639
+ if ("text" in token && typeof token.text === "string") {
640
+ return /* @__PURE__ */ jsx(Text, { children: token.text });
713
641
  }
714
- } catch (err) {
715
- setErrors((prev) => ({ ...prev, details: String(err) }));
716
- } finally {
717
- setLoading((prev) => ({ ...prev, details: false }));
642
+ return null;
643
+ }
644
+ }
645
+ function TableRenderer({ token }) {
646
+ const table = new Table({
647
+ head: token.header.map((cell) => renderInlineToString(cell.tokens)),
648
+ style: { head: ["cyan"], border: ["gray"] }
649
+ });
650
+ for (const row of token.rows) {
651
+ table.push(row.map((cell) => renderInlineToString(cell.tokens)));
652
+ }
653
+ return /* @__PURE__ */ jsx(Text, { children: table.toString() });
654
+ }
655
+ function renderInline(tokens) {
656
+ if (!tokens) return null;
657
+ return tokens.map((token, idx) => {
658
+ switch (token.type) {
659
+ case "text":
660
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
661
+ case "strong":
662
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: renderInline(token.tokens) }, idx);
663
+ case "em":
664
+ return /* @__PURE__ */ jsx(Text, { italic: true, children: renderInline(token.tokens) }, idx);
665
+ case "codespan":
666
+ return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
667
+ "`",
668
+ token.text,
669
+ "`"
670
+ ] }, idx);
671
+ case "link":
672
+ return /* @__PURE__ */ jsx(Link, { url: token.href, children: /* @__PURE__ */ jsx(Text, { color: "blue", children: renderInlineToString(token.tokens) }) }, idx);
673
+ case "image":
674
+ return /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
675
+ "[Image: ",
676
+ token.text || token.href,
677
+ "]"
678
+ ] }, idx);
679
+ case "br":
680
+ return /* @__PURE__ */ jsx(Text, { children: "\n" }, idx);
681
+ case "del":
682
+ return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: renderInline(token.tokens) }, idx);
683
+ default:
684
+ if ("text" in token && typeof token.text === "string") {
685
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
686
+ }
687
+ return null;
718
688
  }
719
- }, [selectedPR, currentRepoSlug]);
720
- useEffect3(() => {
721
- if (!selectedRemote || !currentBranch) return;
722
- const remote = remotes.find((r) => r.name === selectedRemote);
723
- if (!remote) return;
724
- const repo = getRepoFromRemote(remote.url);
725
- if (!repo) return;
726
- setCurrentRepoSlug(repo);
727
- setPrs([]);
728
- setSelectedPR(null);
729
- }, [selectedRemote, currentBranch, remotes]);
730
- useEffect3(() => {
731
- if (currentRepoSlug && currentBranch) {
732
- refreshPRs();
689
+ });
690
+ }
691
+ function renderInlineToString(tokens) {
692
+ if (!tokens) return "";
693
+ return tokens.map((token) => {
694
+ if ("text" in token && typeof token.text === "string") {
695
+ return token.text;
733
696
  }
734
- }, [currentRepoSlug, currentBranch, refreshPRs]);
735
- useEffect3(() => {
736
- if (!selectedPR || !currentRepoSlug) {
737
- setPrDetails(null);
738
- return;
697
+ if ("tokens" in token && Array.isArray(token.tokens)) {
698
+ return renderInlineToString(token.tokens);
739
699
  }
740
- refreshDetails();
741
- }, [selectedPR, currentRepoSlug, refreshDetails]);
742
- const handleRemoteSelect = useCallback(
743
- (remoteName) => {
744
- setSelectedRemote(remoteName);
745
- if (repoPath) {
746
- updateRepoConfig(repoPath, { selectedRemote: remoteName });
700
+ return "";
701
+ }).join("");
702
+ }
703
+
704
+ // src/components/github/PRDetailsBox.tsx
705
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
706
+ function getCheckColor(check) {
707
+ const conclusion = check.conclusion ?? check.state;
708
+ if (conclusion === "SUCCESS") return "green";
709
+ if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
710
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
711
+ if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
712
+ return "yellow";
713
+ if (check.status === "COMPLETED") return "green";
714
+ return void 0;
715
+ }
716
+ function getCheckIcon(check) {
717
+ const conclusion = check.conclusion ?? check.state;
718
+ if (conclusion === "SUCCESS") return "\u2713";
719
+ if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
720
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
721
+ if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
722
+ return "\u25CF";
723
+ if (check.status === "COMPLETED") return "\u2713";
724
+ return "?";
725
+ }
726
+ function PRDetailsBox({ pr, loading, error, isFocused }) {
727
+ var _a, _b, _c, _d, _e, _f, _g;
728
+ const scrollRef = useRef(null);
729
+ const title = "[3] PR Details";
730
+ const borderColor = isFocused ? "yellow" : void 0;
731
+ const displayTitle = pr ? `${title} - #${pr.number}` : title;
732
+ const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
733
+ const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
734
+ const getMergeDisplay = () => {
735
+ if (!pr) return { text: "UNKNOWN", color: "yellow" };
736
+ if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
737
+ if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
738
+ if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
739
+ if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
740
+ return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
741
+ };
742
+ const mergeDisplay = getMergeDisplay();
743
+ useInput(
744
+ (input, key) => {
745
+ var _a2, _b2;
746
+ if (key.upArrow || input === "k") {
747
+ (_a2 = scrollRef.current) == null ? void 0 : _a2.scrollBy(-1);
747
748
  }
748
- },
749
- [repoPath]
750
- );
751
- const handlePRSelect = useCallback((pr) => {
752
- setSelectedPR(pr);
753
- }, []);
754
- const handleCreatePR = useCallback(() => {
755
- exec3("gh pr create --web", () => {
756
- process.stdout.emit("resize");
757
- });
758
- }, []);
759
- useInput4(
760
- (input) => {
761
- if (input === "1") setFocusedBox("remotes");
762
- if (input === "2") setFocusedBox("prs");
763
- if (input === "3") setFocusedBox("details");
764
- if (input === "r") {
765
- if (focusedBox === "prs") refreshPRs();
766
- if (focusedBox === "details") refreshDetails();
749
+ if (key.downArrow || input === "j") {
750
+ (_b2 = scrollRef.current) == null ? void 0 : _b2.scrollBy(1);
751
+ }
752
+ if (input === "o" && (pr == null ? void 0 : pr.url)) {
753
+ open(pr.url).catch(() => {
754
+ });
767
755
  }
768
756
  },
769
757
  { isActive: isFocused }
770
758
  );
771
- if (isRepo === false) {
772
- return /* @__PURE__ */ jsx5(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
773
- }
774
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
775
- /* @__PURE__ */ jsx5(
776
- RemotesBox,
777
- {
778
- remotes,
779
- selectedRemote,
780
- onSelect: handleRemoteSelect,
781
- loading: loading.remotes,
782
- error: errors.remotes,
783
- isFocused: isFocused && focusedBox === "remotes"
784
- }
785
- ),
786
- /* @__PURE__ */ jsx5(
787
- PullRequestsBox,
788
- {
789
- prs,
790
- selectedPR,
791
- onSelect: handlePRSelect,
792
- onCreatePR: handleCreatePR,
793
- loading: loading.prs,
794
- error: errors.prs,
795
- branch: currentBranch,
796
- repoSlug: currentRepoSlug,
797
- isFocused: isFocused && focusedBox === "prs"
798
- }
799
- ),
800
- /* @__PURE__ */ jsx5(
801
- PRDetailsBox,
802
- {
803
- pr: prDetails,
804
- loading: loading.details,
805
- error: errors.details,
806
- isFocused: isFocused && focusedBox === "details"
807
- }
808
- )
809
- ] });
759
+ return /* @__PURE__ */ jsx2(TitledBox, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx2(ScrollView, { ref: scrollRef, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
760
+ loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
761
+ error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
762
+ !loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
763
+ !loading && !error && pr && /* @__PURE__ */ jsxs2(Fragment, { children: [
764
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: pr.title }),
765
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
766
+ "by ",
767
+ ((_a = pr.author) == null ? void 0 : _a.login) ?? "unknown",
768
+ " | ",
769
+ ((_b = pr.commits) == null ? void 0 : _b.length) ?? 0,
770
+ " commits"
771
+ ] }),
772
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
773
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
774
+ /* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
775
+ /* @__PURE__ */ jsx2(Text2, { children: " | " }),
776
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
777
+ /* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
778
+ ] }),
779
+ (((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
780
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Assignees: " }),
781
+ /* @__PURE__ */ jsx2(Text2, { children: pr.assignees.map((a) => a.login).join(", ") })
782
+ ] }),
783
+ (((_d = pr.reviews) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
784
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reviews:" }),
785
+ pr.reviews.map((review, idx) => {
786
+ const color = review.state === "APPROVED" ? "green" : review.state === "CHANGES_REQUESTED" ? "red" : review.state === "COMMENTED" ? "blue" : "yellow";
787
+ const icon = review.state === "APPROVED" ? "\u2713" : review.state === "CHANGES_REQUESTED" ? "\u2717" : review.state === "COMMENTED" ? "\u{1F4AC}" : "\u25CB";
788
+ return /* @__PURE__ */ jsxs2(Text2, { color, children: [
789
+ " ",
790
+ icon,
791
+ " ",
792
+ review.author.login
793
+ ] }, idx);
794
+ })
795
+ ] }),
796
+ (((_e = pr.reviewRequests) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { children: [
797
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Pending: " }),
798
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
799
+ ] }),
800
+ (((_f = pr.statusCheckRollup) == null ? void 0 : _f.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
801
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Checks:" }),
802
+ (_g = pr.statusCheckRollup) == null ? void 0 : _g.map((check, idx) => /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
803
+ " ",
804
+ getCheckIcon(check),
805
+ " ",
806
+ check.name ?? check.context
807
+ ] }, idx))
808
+ ] }),
809
+ pr.body && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
810
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
811
+ /* @__PURE__ */ jsx2(Markdown, { children: pr.body })
812
+ ] })
813
+ ] })
814
+ ] }) }) }) });
810
815
  }
811
816
 
812
- // src/components/jira/JiraView.tsx
813
- import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
814
- import open2 from "open";
815
- import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
816
- import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
817
+ // src/components/github/PullRequestsBox.tsx
818
+ import { useEffect, useState } from "react";
819
+ import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
820
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
817
821
 
818
- // src/lib/jira/parser.ts
819
- var TICKET_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
820
- function isValidTicketKeyFormat(key) {
821
- return TICKET_KEY_PATTERN.test(key.toUpperCase());
822
- }
823
- function parseTicketKey(input) {
824
- const trimmed = input.trim();
825
- const urlMatch = trimmed.match(/\/browse\/([A-Za-z][A-Za-z0-9]+-\d+)/i);
826
- if (urlMatch) {
827
- return urlMatch[1].toUpperCase();
828
- }
829
- const upperInput = trimmed.toUpperCase();
830
- if (isValidTicketKeyFormat(upperInput)) {
831
- return upperInput;
832
- }
833
- return null;
834
- }
835
- function extractTicketKeyFromBranch(branchName) {
836
- const match = branchName.match(/([A-Za-z][A-Za-z0-9]+-\d+)/);
837
- if (match) {
838
- const candidate = match[1].toUpperCase();
839
- if (isValidTicketKeyFormat(candidate)) {
840
- return candidate;
841
- }
822
+ // src/lib/clipboard.ts
823
+ import { exec as exec2 } from "child_process";
824
+ async function copyToClipboard(text) {
825
+ var _a, _b;
826
+ const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
827
+ try {
828
+ const child = exec2(command);
829
+ (_a = child.stdin) == null ? void 0 : _a.write(text);
830
+ (_b = child.stdin) == null ? void 0 : _b.end();
831
+ await new Promise((resolve, reject) => {
832
+ child.on("close", (code) => {
833
+ if (code === 0) resolve();
834
+ else reject(new Error(`Clipboard command exited with code ${code}`));
835
+ });
836
+ });
837
+ return true;
838
+ } catch {
839
+ return false;
842
840
  }
843
- return null;
844
841
  }
845
842
 
846
- // src/lib/jira/config.ts
847
- function isJiraConfigured(repoPath) {
848
- const config = getRepoConfig(repoPath);
849
- return !!(config.jiraSiteUrl && config.jiraEmail && config.jiraApiToken);
850
- }
851
- function getJiraSiteUrl(repoPath) {
852
- const config = getRepoConfig(repoPath);
853
- return config.jiraSiteUrl ?? null;
854
- }
855
- function setJiraSiteUrl(repoPath, siteUrl) {
856
- updateRepoConfig(repoPath, { jiraSiteUrl: siteUrl });
857
- }
858
- function getJiraCredentials(repoPath) {
859
- const config = getRepoConfig(repoPath);
860
- return {
861
- email: config.jiraEmail ?? null,
862
- apiToken: config.jiraApiToken ?? null
863
- };
864
- }
865
- function setJiraCredentials(repoPath, email, apiToken) {
866
- updateRepoConfig(repoPath, { jiraEmail: email, jiraApiToken: apiToken });
867
- }
868
- function getLinkedTickets(repoPath, branch) {
869
- var _a;
870
- const config = getRepoConfig(repoPath);
871
- return ((_a = config.branchTickets) == null ? void 0 : _a[branch]) ?? [];
843
+ // src/components/github/PullRequestsBox.tsx
844
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
845
+ function PullRequestsBox({
846
+ prs,
847
+ selectedPR,
848
+ onSelect,
849
+ onCreatePR,
850
+ loading,
851
+ error,
852
+ branch,
853
+ repoSlug,
854
+ isFocused
855
+ }) {
856
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
857
+ const totalItems = prs.length + 1;
858
+ useEffect(() => {
859
+ const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
860
+ if (idx >= 0) setHighlightedIndex(idx);
861
+ }, [selectedPR, prs]);
862
+ useInput2(
863
+ (input, key) => {
864
+ if (!isFocused) return;
865
+ if (key.upArrow || input === "k") {
866
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
867
+ }
868
+ if (key.downArrow || input === "j") {
869
+ setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
870
+ }
871
+ if (key.return) {
872
+ if (highlightedIndex === prs.length) {
873
+ onCreatePR();
874
+ } else if (prs[highlightedIndex]) {
875
+ onSelect(prs[highlightedIndex]);
876
+ }
877
+ }
878
+ if (input === "y" && repoSlug && prs[highlightedIndex]) {
879
+ const pr = prs[highlightedIndex];
880
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
881
+ copyToClipboard(url);
882
+ }
883
+ },
884
+ { isActive: isFocused }
885
+ );
886
+ const title = "[2] Pull Requests";
887
+ const subtitle = branch ? ` (${branch})` : "";
888
+ const borderColor = isFocused ? "yellow" : void 0;
889
+ return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
890
+ loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
891
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
892
+ !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
893
+ prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
894
+ prs.map((pr, idx) => {
895
+ const isHighlighted = isFocused && idx === highlightedIndex;
896
+ const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
897
+ const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
898
+ return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
899
+ prefix,
900
+ "#",
901
+ pr.number,
902
+ " ",
903
+ pr.isDraft ? "[Draft] " : "",
904
+ pr.title
905
+ ] }, pr.number);
906
+ }),
907
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
908
+ isFocused && highlightedIndex === prs.length ? "> " : " ",
909
+ "+ Create new PR"
910
+ ] })
911
+ ] })
912
+ ] }) });
872
913
  }
873
- function addLinkedTicket(repoPath, branch, ticket) {
874
- const config = getRepoConfig(repoPath);
875
- const branchTickets = config.branchTickets ?? {};
876
- const tickets = branchTickets[branch] ?? [];
877
- if (tickets.some((t) => t.key === ticket.key)) {
878
- return;
879
- }
880
- updateRepoConfig(repoPath, {
881
- branchTickets: {
882
- ...branchTickets,
883
- [branch]: [...tickets, ticket]
884
- }
885
- });
914
+
915
+ // src/components/github/RemotesBox.tsx
916
+ import { useEffect as useEffect2, useState as useState2 } from "react";
917
+ import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
918
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
919
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
920
+ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
921
+ const [highlightedIndex, setHighlightedIndex] = useState2(0);
922
+ useEffect2(() => {
923
+ const idx = remotes.findIndex((r) => r.name === selectedRemote);
924
+ if (idx >= 0) setHighlightedIndex(idx);
925
+ }, [selectedRemote, remotes]);
926
+ useInput3(
927
+ (input, key) => {
928
+ if (!isFocused || remotes.length === 0) return;
929
+ if (key.upArrow || input === "k") {
930
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
931
+ }
932
+ if (key.downArrow || input === "j") {
933
+ setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
934
+ }
935
+ if (key.return) {
936
+ onSelect(remotes[highlightedIndex].name);
937
+ }
938
+ },
939
+ { isActive: isFocused }
940
+ );
941
+ const title = "[1] Remotes";
942
+ const borderColor = isFocused ? "yellow" : void 0;
943
+ return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
944
+ loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
945
+ error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
946
+ !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
947
+ !loading && !error && remotes.map((remote, idx) => {
948
+ const isHighlighted = isFocused && idx === highlightedIndex;
949
+ const isSelected = remote.name === selectedRemote;
950
+ const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
951
+ return /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
952
+ prefix,
953
+ remote.name,
954
+ " (",
955
+ remote.url,
956
+ ")"
957
+ ] }, remote.name);
958
+ })
959
+ ] }) });
886
960
  }
887
- function removeLinkedTicket(repoPath, branch, ticketKey) {
888
- const config = getRepoConfig(repoPath);
889
- const branchTickets = config.branchTickets ?? {};
890
- const tickets = branchTickets[branch] ?? [];
891
- updateRepoConfig(repoPath, {
892
- branchTickets: {
893
- ...branchTickets,
894
- [branch]: tickets.filter((t) => t.key !== ticketKey)
895
- }
961
+
962
+ // src/components/github/GitHubView.tsx
963
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
964
+ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
965
+ const [isRepo, setIsRepo] = useState3(null);
966
+ const [repoPath, setRepoPath] = useState3(null);
967
+ const [remotes, setRemotes] = useState3([]);
968
+ const [currentBranch, setCurrentBranch] = useState3(null);
969
+ const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
970
+ const [selectedRemote, setSelectedRemote] = useState3(null);
971
+ const [selectedPR, setSelectedPR] = useState3(null);
972
+ const [prs, setPrs] = useState3([]);
973
+ const [prDetails, setPrDetails] = useState3(null);
974
+ const [loading, setLoading] = useState3({
975
+ remotes: true,
976
+ prs: false,
977
+ details: false
896
978
  });
897
- }
898
- function updateTicketStatus(repoPath, branch, ticketKey, newStatus) {
899
- const config = getRepoConfig(repoPath);
900
- const branchTickets = config.branchTickets ?? {};
901
- const tickets = branchTickets[branch] ?? [];
902
- updateRepoConfig(repoPath, {
903
- branchTickets: {
904
- ...branchTickets,
905
- [branch]: tickets.map((t) => t.key === ticketKey ? { ...t, status: newStatus } : t)
979
+ const [errors, setErrors] = useState3({});
980
+ const [focusedBox, setFocusedBox] = useState3("remotes");
981
+ useEffect3(() => {
982
+ if (!isFocused) {
983
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
984
+ return;
906
985
  }
907
- });
908
- }
909
-
910
- // src/lib/jira/api.ts
911
- function createAuthHeader(email, apiToken) {
912
- const credentials = Buffer.from(`${email}:${apiToken}`).toString("base64");
913
- return `Basic ${credentials}`;
914
- }
915
- async function jiraFetch(auth, endpoint, options) {
916
- const url = `${auth.siteUrl}/rest/api/3${endpoint}`;
917
- const method = (options == null ? void 0 : options.method) ?? "GET";
918
- try {
919
- const headers = {
920
- Authorization: createAuthHeader(auth.email, auth.apiToken),
921
- Accept: "application/json"
922
- };
923
- const fetchOptions = { method, headers };
924
- if (options == null ? void 0 : options.body) {
925
- headers["Content-Type"] = "application/json";
926
- fetchOptions.body = JSON.stringify(options.body);
986
+ const bindings = [];
987
+ if (focusedBox === "remotes") {
988
+ bindings.push({ key: "Enter", label: "Select Remote" });
989
+ } else if (focusedBox === "prs") {
990
+ bindings.push({ key: "n", label: "New PR", color: "green" });
991
+ bindings.push({ key: "r", label: "Refresh" });
992
+ bindings.push({ key: "o", label: "Open", color: "green" });
993
+ bindings.push({ key: "y", label: "Copy Link" });
994
+ } else if (focusedBox === "details") {
995
+ bindings.push({ key: "r", label: "Refresh" });
996
+ bindings.push({ key: "o", label: "Open", color: "green" });
927
997
  }
928
- const response = await fetch(url, fetchOptions);
929
- if (!response.ok) {
930
- const text = await response.text();
931
- return { ok: false, status: response.status, error: text };
998
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
999
+ }, [isFocused, focusedBox, onKeybindingsChange]);
1000
+ useEffect3(() => {
1001
+ const gitRepoCheck = isGitRepo();
1002
+ setIsRepo(gitRepoCheck);
1003
+ if (!gitRepoCheck) {
1004
+ setLoading((prev) => ({ ...prev, remotes: false }));
1005
+ setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
1006
+ return;
932
1007
  }
933
- if (response.status === 204) {
934
- return { ok: true, status: response.status, data: null };
1008
+ const rootResult = getRepoRoot();
1009
+ if (rootResult.success) {
1010
+ setRepoPath(rootResult.data);
935
1011
  }
936
- const data = await response.json();
937
- return { ok: true, status: response.status, data };
938
- } catch (err) {
939
- const message = err instanceof Error ? err.message : "Network error";
940
- return { ok: false, status: 0, error: message };
941
- }
942
- }
943
- async function validateCredentials(auth) {
944
- const result = await jiraFetch(auth, "/myself");
945
- if (!result.ok) {
946
- if (result.status === 401 || result.status === 403) {
947
- return {
948
- success: false,
949
- error: "Invalid credentials. Check your email and API token.",
950
- errorType: "auth_error"
951
- };
1012
+ const branchResult = getCurrentBranch();
1013
+ if (branchResult.success) {
1014
+ setCurrentBranch(branchResult.data);
952
1015
  }
953
- return {
954
- success: false,
955
- error: result.error ?? "Failed to connect to Jira",
956
- errorType: "api_error"
957
- };
958
- }
959
- return { success: true, data: result.data };
960
- }
961
- async function getIssue(auth, ticketKey) {
962
- const result = await jiraFetch(auth, `/issue/${ticketKey}?fields=summary,status`);
963
- if (!result.ok) {
964
- if (result.status === 401 || result.status === 403) {
965
- return {
966
- success: false,
967
- error: "Authentication failed",
968
- errorType: "auth_error"
969
- };
1016
+ const remotesResult = listRemotes();
1017
+ if (remotesResult.success) {
1018
+ setRemotes(remotesResult.data);
1019
+ const remoteNames = remotesResult.data.map((r) => r.name);
1020
+ const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
1021
+ setSelectedRemote(defaultRemote);
1022
+ } else {
1023
+ setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
970
1024
  }
971
- if (result.status === 404) {
972
- return {
973
- success: false,
974
- error: `Ticket ${ticketKey} not found`,
975
- errorType: "invalid_ticket"
976
- };
1025
+ setLoading((prev) => ({ ...prev, remotes: false }));
1026
+ }, []);
1027
+ const refreshPRs = useCallback(async () => {
1028
+ if (!currentBranch || !currentRepoSlug) return;
1029
+ setLoading((prev) => ({ ...prev, prs: true }));
1030
+ try {
1031
+ const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1032
+ if (result.success) {
1033
+ setPrs(result.data);
1034
+ if (result.data.length > 0) {
1035
+ setSelectedPR((prev) => prev ?? result.data[0]);
1036
+ }
1037
+ setErrors((prev) => ({ ...prev, prs: void 0 }));
1038
+ } else {
1039
+ setErrors((prev) => ({ ...prev, prs: result.error }));
1040
+ }
1041
+ } catch (err) {
1042
+ setErrors((prev) => ({ ...prev, prs: String(err) }));
1043
+ } finally {
1044
+ setLoading((prev) => ({ ...prev, prs: false }));
977
1045
  }
978
- return {
979
- success: false,
980
- error: result.error ?? "Failed to fetch issue",
981
- errorType: "api_error"
982
- };
983
- }
984
- return { success: true, data: result.data };
985
- }
986
- async function getTransitions(auth, ticketKey) {
987
- const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`);
988
- if (!result.ok) {
989
- if (result.status === 401 || result.status === 403) {
990
- return {
991
- success: false,
992
- error: "Authentication failed",
993
- errorType: "auth_error"
994
- };
1046
+ }, [currentBranch, currentRepoSlug]);
1047
+ const refreshDetails = useCallback(async () => {
1048
+ if (!selectedPR || !currentRepoSlug) return;
1049
+ setLoading((prev) => ({ ...prev, details: true }));
1050
+ try {
1051
+ const result = await getPRDetails(selectedPR.number, currentRepoSlug);
1052
+ if (result.success) {
1053
+ setPrDetails(result.data);
1054
+ setErrors((prev) => ({ ...prev, details: void 0 }));
1055
+ } else {
1056
+ setErrors((prev) => ({ ...prev, details: result.error }));
1057
+ }
1058
+ } catch (err) {
1059
+ setErrors((prev) => ({ ...prev, details: String(err) }));
1060
+ } finally {
1061
+ setLoading((prev) => ({ ...prev, details: false }));
995
1062
  }
996
- if (result.status === 404) {
997
- return {
998
- success: false,
999
- error: `Ticket ${ticketKey} not found`,
1000
- errorType: "invalid_ticket"
1001
- };
1063
+ }, [selectedPR, currentRepoSlug]);
1064
+ useEffect3(() => {
1065
+ if (!selectedRemote || !currentBranch) return;
1066
+ const remote = remotes.find((r) => r.name === selectedRemote);
1067
+ if (!remote) return;
1068
+ const repo = getRepoFromRemote(remote.url);
1069
+ if (!repo) return;
1070
+ setCurrentRepoSlug(repo);
1071
+ setPrs([]);
1072
+ setSelectedPR(null);
1073
+ }, [selectedRemote, currentBranch, remotes]);
1074
+ useEffect3(() => {
1075
+ if (currentRepoSlug && currentBranch) {
1076
+ refreshPRs();
1002
1077
  }
1003
- return {
1004
- success: false,
1005
- error: result.error ?? "Failed to fetch transitions",
1006
- errorType: "api_error"
1007
- };
1008
- }
1009
- const data = result.data;
1010
- return { success: true, data: data.transitions };
1011
- }
1012
- async function applyTransition(auth, ticketKey, transitionId) {
1013
- const result = await jiraFetch(auth, `/issue/${ticketKey}/transitions`, {
1014
- method: "POST",
1015
- body: { transition: { id: transitionId } }
1016
- });
1017
- if (!result.ok) {
1018
- if (result.status === 401 || result.status === 403) {
1019
- return {
1020
- success: false,
1021
- error: "Authentication failed",
1022
- errorType: "auth_error"
1023
- };
1078
+ }, [currentRepoSlug, currentBranch, refreshPRs]);
1079
+ useEffect3(() => {
1080
+ if (!selectedPR || !currentRepoSlug) {
1081
+ setPrDetails(null);
1082
+ return;
1024
1083
  }
1025
- if (result.status === 404) {
1026
- return {
1027
- success: false,
1028
- error: `Ticket ${ticketKey} not found`,
1029
- errorType: "invalid_ticket"
1030
- };
1084
+ refreshDetails();
1085
+ }, [selectedPR, currentRepoSlug, refreshDetails]);
1086
+ const handleRemoteSelect = useCallback(
1087
+ (remoteName) => {
1088
+ setSelectedRemote(remoteName);
1089
+ if (repoPath) {
1090
+ updateRepoConfig(repoPath, { selectedRemote: remoteName });
1091
+ }
1092
+ },
1093
+ [repoPath]
1094
+ );
1095
+ const handlePRSelect = useCallback((pr) => {
1096
+ setSelectedPR(pr);
1097
+ }, []);
1098
+ const prNumbersBeforeCreate = useRef2(/* @__PURE__ */ new Set());
1099
+ const pollingIntervalRef = useRef2(null);
1100
+ const handleCreatePR = useCallback(() => {
1101
+ prNumbersBeforeCreate.current = new Set(prs.map((pr) => pr.number));
1102
+ exec3("gh pr create --web", () => {
1103
+ process.stdout.emit("resize");
1104
+ });
1105
+ if (!currentBranch || !currentRepoSlug) return;
1106
+ let attempts = 0;
1107
+ const maxAttempts = 24;
1108
+ const pollInterval = 5e3;
1109
+ if (pollingIntervalRef.current) {
1110
+ clearInterval(pollingIntervalRef.current);
1031
1111
  }
1032
- return {
1033
- success: false,
1034
- error: result.error ?? "Failed to apply transition",
1035
- errorType: "api_error"
1112
+ pollingIntervalRef.current = setInterval(async () => {
1113
+ attempts++;
1114
+ if (attempts > maxAttempts) {
1115
+ if (pollingIntervalRef.current) {
1116
+ clearInterval(pollingIntervalRef.current);
1117
+ pollingIntervalRef.current = null;
1118
+ }
1119
+ return;
1120
+ }
1121
+ const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1122
+ if (result.success) {
1123
+ setPrs(result.data);
1124
+ const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
1125
+ if (newPR) {
1126
+ if (pollingIntervalRef.current) {
1127
+ clearInterval(pollingIntervalRef.current);
1128
+ pollingIntervalRef.current = null;
1129
+ }
1130
+ const tickets = repoPath && currentBranch ? getLinkedTickets(repoPath, currentBranch).map((t) => t.key) : [];
1131
+ logPRCreated(newPR.number, newPR.title, tickets);
1132
+ onLogUpdated == null ? void 0 : onLogUpdated();
1133
+ setSelectedPR(newPR);
1134
+ }
1135
+ }
1136
+ }, pollInterval);
1137
+ }, [prs, currentBranch, currentRepoSlug, repoPath, onLogUpdated]);
1138
+ useEffect3(() => {
1139
+ return () => {
1140
+ if (pollingIntervalRef.current) {
1141
+ clearInterval(pollingIntervalRef.current);
1142
+ }
1036
1143
  };
1144
+ }, []);
1145
+ useInput4(
1146
+ (input) => {
1147
+ if (input === "1") setFocusedBox("remotes");
1148
+ if (input === "2") setFocusedBox("prs");
1149
+ if (input === "3") setFocusedBox("details");
1150
+ if (input === "r") {
1151
+ if (focusedBox === "prs") refreshPRs();
1152
+ if (focusedBox === "details") refreshDetails();
1153
+ }
1154
+ },
1155
+ { isActive: isFocused }
1156
+ );
1157
+ if (isRepo === false) {
1158
+ return /* @__PURE__ */ jsx5(TitledBox4, { borderStyle: "round", titles: ["Error"], flexGrow: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", children: "Current directory is not a git repository" }) });
1037
1159
  }
1038
- return { success: true, data: null };
1160
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
1161
+ /* @__PURE__ */ jsx5(
1162
+ RemotesBox,
1163
+ {
1164
+ remotes,
1165
+ selectedRemote,
1166
+ onSelect: handleRemoteSelect,
1167
+ loading: loading.remotes,
1168
+ error: errors.remotes,
1169
+ isFocused: isFocused && focusedBox === "remotes"
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsx5(
1173
+ PullRequestsBox,
1174
+ {
1175
+ prs,
1176
+ selectedPR,
1177
+ onSelect: handlePRSelect,
1178
+ onCreatePR: handleCreatePR,
1179
+ loading: loading.prs,
1180
+ error: errors.prs,
1181
+ branch: currentBranch,
1182
+ repoSlug: currentRepoSlug,
1183
+ isFocused: isFocused && focusedBox === "prs"
1184
+ }
1185
+ ),
1186
+ /* @__PURE__ */ jsx5(
1187
+ PRDetailsBox,
1188
+ {
1189
+ pr: prDetails,
1190
+ loading: loading.details,
1191
+ error: errors.details,
1192
+ isFocused: isFocused && focusedBox === "details"
1193
+ }
1194
+ )
1195
+ ] });
1039
1196
  }
1040
1197
 
1198
+ // src/components/jira/JiraView.tsx
1199
+ import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
1200
+ import open2 from "open";
1201
+ import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
1202
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1203
+
1041
1204
  // src/components/jira/ChangeStatusModal.tsx
1042
1205
  import { useEffect as useEffect4, useState as useState4 } from "react";
1043
1206
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
@@ -1121,17 +1284,17 @@ import { useState as useState5 } from "react";
1121
1284
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1122
1285
 
1123
1286
  // src/lib/editor.ts
1124
- import { spawnSync } from "child_process";
1125
- import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1287
+ import { spawnSync as spawnSync2 } from "child_process";
1288
+ import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
1126
1289
  import { tmpdir } from "os";
1127
- import { join as join2 } from "path";
1290
+ import { join as join3 } from "path";
1128
1291
  function openInEditor(content, filename) {
1129
1292
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
1130
- const tempDir = mkdtempSync(join2(tmpdir(), "clairo-"));
1131
- const tempFile = join2(tempDir, filename);
1293
+ const tempDir = mkdtempSync(join3(tmpdir(), "clairo-"));
1294
+ const tempFile = join3(tempDir, filename);
1132
1295
  try {
1133
- writeFileSync2(tempFile, content);
1134
- const result = spawnSync(editor, [tempFile], {
1296
+ writeFileSync3(tempFile, content);
1297
+ const result = spawnSync2(editor, [tempFile], {
1135
1298
  stdio: "inherit"
1136
1299
  });
1137
1300
  process.stdout.write("\x1B[2J\x1B[H");
@@ -1139,7 +1302,7 @@ function openInEditor(content, filename) {
1139
1302
  if (result.status !== 0) {
1140
1303
  return null;
1141
1304
  }
1142
- return readFileSync2(tempFile, "utf-8");
1305
+ return readFileSync3(tempFile, "utf-8");
1143
1306
  } finally {
1144
1307
  try {
1145
1308
  rmSync(tempDir, { recursive: true });
@@ -1335,7 +1498,7 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
1335
1498
 
1336
1499
  // src/components/jira/JiraView.tsx
1337
1500
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1338
- function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1501
+ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated }) {
1339
1502
  const [repoPath, setRepoPath] = useState7(null);
1340
1503
  const [currentBranch, setCurrentBranch] = useState7(null);
1341
1504
  const [isRepo, setIsRepo] = useState7(null);
@@ -1592,7 +1755,10 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1592
1755
  ticketKey: ticket.key,
1593
1756
  currentStatus: ticket.status,
1594
1757
  onComplete: (newStatus) => {
1758
+ const oldStatus = ticket.status;
1595
1759
  updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
1760
+ logJiraStatusChanged(ticket.key, ticket.summary, oldStatus, newStatus);
1761
+ onLogUpdated == null ? void 0 : onLogUpdated();
1596
1762
  setShowStatusModal(false);
1597
1763
  refreshTickets();
1598
1764
  },
@@ -1618,9 +1784,237 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1618
1784
  ] }) });
1619
1785
  }
1620
1786
 
1621
- // src/components/ui/KeybindingsBar.tsx
1622
- import { Box as Box12, Text as Text12 } from "ink";
1787
+ // src/components/logs/LogsView.tsx
1788
+ import { useCallback as useCallback3, useEffect as useEffect6, useState as useState8 } from "react";
1789
+ import { Box as Box14, useInput as useInput12 } from "ink";
1790
+
1791
+ // src/components/logs/LogsHistoryBox.tsx
1792
+ import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
1793
+ import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
1623
1794
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1795
+ function LogsHistoryBox({
1796
+ logFiles,
1797
+ selectedDate,
1798
+ highlightedIndex,
1799
+ onHighlight,
1800
+ onSelect,
1801
+ isFocused
1802
+ }) {
1803
+ const title = "[5] Logs";
1804
+ const borderColor = isFocused ? "yellow" : void 0;
1805
+ useInput10(
1806
+ (input, key) => {
1807
+ if (logFiles.length === 0) return;
1808
+ if (key.upArrow || input === "k") {
1809
+ onHighlight(Math.max(0, highlightedIndex - 1));
1810
+ }
1811
+ if (key.downArrow || input === "j") {
1812
+ onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
1813
+ }
1814
+ if (key.return) {
1815
+ const file = logFiles[highlightedIndex];
1816
+ if (file) {
1817
+ onSelect(file.date);
1818
+ }
1819
+ }
1820
+ },
1821
+ { isActive: isFocused }
1822
+ );
1823
+ return /* @__PURE__ */ jsx12(TitledBox6, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
1824
+ logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
1825
+ logFiles.map((file, idx) => {
1826
+ const isHighlighted = idx === highlightedIndex;
1827
+ const isSelected = file.date === selectedDate;
1828
+ const cursor = isHighlighted ? ">" : " ";
1829
+ const indicator = isSelected ? " *" : "";
1830
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
1831
+ /* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
1832
+ cursor,
1833
+ " "
1834
+ ] }),
1835
+ /* @__PURE__ */ jsx12(
1836
+ Text12,
1837
+ {
1838
+ color: file.isToday ? "green" : void 0,
1839
+ bold: file.isToday,
1840
+ children: file.date
1841
+ }
1842
+ ),
1843
+ file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
1844
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
1845
+ ] }, file.date);
1846
+ })
1847
+ ] }) });
1848
+ }
1849
+
1850
+ // src/components/logs/LogViewerBox.tsx
1851
+ import { useRef as useRef3 } from "react";
1852
+ import { TitledBox as TitledBox7 } from "@mishieck/ink-titled-box";
1853
+ import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
1854
+ import { ScrollView as ScrollView2 } from "ink-scroll-view";
1855
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1856
+ function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
1857
+ const scrollRef = useRef3(null);
1858
+ const title = "[6] Log Content";
1859
+ const borderColor = isFocused ? "yellow" : void 0;
1860
+ const displayTitle = date ? `${title} - ${date}.md` : title;
1861
+ useInput11(
1862
+ (input, key) => {
1863
+ var _a, _b;
1864
+ if (key.upArrow || input === "k") {
1865
+ (_a = scrollRef.current) == null ? void 0 : _a.scrollBy(-1);
1866
+ }
1867
+ if (key.downArrow || input === "j") {
1868
+ (_b = scrollRef.current) == null ? void 0 : _b.scrollBy(1);
1869
+ }
1870
+ if (input === "e" && date) {
1871
+ openLogInEditor(date);
1872
+ onRefresh();
1873
+ }
1874
+ if (input === "n") {
1875
+ const today = getTodayDate();
1876
+ if (!logExists(today)) {
1877
+ createEmptyLog(today);
1878
+ onLogCreated();
1879
+ }
1880
+ }
1881
+ if (input === "r") {
1882
+ onRefresh();
1883
+ }
1884
+ },
1885
+ { isActive: isFocused }
1886
+ );
1887
+ return /* @__PURE__ */ jsx13(TitledBox7, { borderStyle: "round", titles: [displayTitle], borderColor, flexGrow: 1, children: /* @__PURE__ */ jsx13(Box13, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx13(ScrollView2, { ref: scrollRef, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
1888
+ !date && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Select a log file to view" }),
1889
+ date && content === null && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Log file not found" }),
1890
+ date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Empty log file" }),
1891
+ date && content && content.trim() !== "" && /* @__PURE__ */ jsx13(Markdown, { children: content })
1892
+ ] }) }) }) });
1893
+ }
1894
+
1895
+ // src/components/logs/LogsView.tsx
1896
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1897
+ function LogsView({ isFocused, onKeybindingsChange, refreshKey }) {
1898
+ const [logFiles, setLogFiles] = useState8([]);
1899
+ const [selectedDate, setSelectedDate] = useState8(null);
1900
+ const [logContent, setLogContent] = useState8(null);
1901
+ const [highlightedIndex, setHighlightedIndex] = useState8(0);
1902
+ const [focusedBox, setFocusedBox] = useState8("history");
1903
+ useEffect6(() => {
1904
+ if (!isFocused) {
1905
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
1906
+ return;
1907
+ }
1908
+ const bindings = [];
1909
+ if (focusedBox === "history") {
1910
+ bindings.push({ key: "Enter", label: "Select" });
1911
+ } else if (focusedBox === "viewer") {
1912
+ bindings.push({ key: "e", label: "Edit" });
1913
+ bindings.push({ key: "n", label: "New Log", color: "green" });
1914
+ bindings.push({ key: "r", label: "Refresh" });
1915
+ }
1916
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
1917
+ }, [isFocused, focusedBox, onKeybindingsChange]);
1918
+ const refreshLogFiles = useCallback3(() => {
1919
+ const files = listLogFiles();
1920
+ setLogFiles(files);
1921
+ if (files.length > 0 && !selectedDate) {
1922
+ const today = getTodayDate();
1923
+ const todayFile = files.find((f) => f.date === today);
1924
+ if (todayFile) {
1925
+ setSelectedDate(todayFile.date);
1926
+ const idx = files.findIndex((f) => f.date === today);
1927
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1928
+ } else {
1929
+ setSelectedDate(files[0].date);
1930
+ setHighlightedIndex(0);
1931
+ }
1932
+ }
1933
+ }, [selectedDate]);
1934
+ useEffect6(() => {
1935
+ refreshLogFiles();
1936
+ }, [refreshLogFiles]);
1937
+ useEffect6(() => {
1938
+ if (selectedDate) {
1939
+ const content = readLog(selectedDate);
1940
+ setLogContent(content);
1941
+ } else {
1942
+ setLogContent(null);
1943
+ }
1944
+ }, [selectedDate]);
1945
+ useEffect6(() => {
1946
+ if (refreshKey !== void 0 && refreshKey > 0) {
1947
+ const files = listLogFiles();
1948
+ setLogFiles(files);
1949
+ const today = getTodayDate();
1950
+ if (selectedDate === today) {
1951
+ const content = readLog(today);
1952
+ setLogContent(content);
1953
+ } else if (!selectedDate && files.length > 0) {
1954
+ const todayFile = files.find((f) => f.date === today);
1955
+ if (todayFile) {
1956
+ setSelectedDate(today);
1957
+ const idx = files.findIndex((f) => f.date === today);
1958
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1959
+ }
1960
+ }
1961
+ }
1962
+ }, [refreshKey, selectedDate]);
1963
+ const handleSelectDate = useCallback3((date) => {
1964
+ setSelectedDate(date);
1965
+ }, []);
1966
+ const handleRefresh = useCallback3(() => {
1967
+ refreshLogFiles();
1968
+ if (selectedDate) {
1969
+ const content = readLog(selectedDate);
1970
+ setLogContent(content);
1971
+ }
1972
+ }, [refreshLogFiles, selectedDate]);
1973
+ const handleLogCreated = useCallback3(() => {
1974
+ const files = listLogFiles();
1975
+ setLogFiles(files);
1976
+ const today = getTodayDate();
1977
+ setSelectedDate(today);
1978
+ const idx = files.findIndex((f) => f.date === today);
1979
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1980
+ const content = readLog(today);
1981
+ setLogContent(content);
1982
+ }, []);
1983
+ useInput12(
1984
+ (input) => {
1985
+ if (input === "5") setFocusedBox("history");
1986
+ if (input === "6") setFocusedBox("viewer");
1987
+ },
1988
+ { isActive: isFocused }
1989
+ );
1990
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", flexGrow: 1, children: [
1991
+ /* @__PURE__ */ jsx14(
1992
+ LogsHistoryBox,
1993
+ {
1994
+ logFiles,
1995
+ selectedDate,
1996
+ highlightedIndex,
1997
+ onHighlight: setHighlightedIndex,
1998
+ onSelect: handleSelectDate,
1999
+ isFocused: isFocused && focusedBox === "history"
2000
+ }
2001
+ ),
2002
+ /* @__PURE__ */ jsx14(
2003
+ LogViewerBox,
2004
+ {
2005
+ date: selectedDate,
2006
+ content: logContent,
2007
+ isFocused: isFocused && focusedBox === "viewer",
2008
+ onRefresh: handleRefresh,
2009
+ onLogCreated: handleLogCreated
2010
+ }
2011
+ )
2012
+ ] });
2013
+ }
2014
+
2015
+ // src/components/ui/KeybindingsBar.tsx
2016
+ import { Box as Box15, Text as Text14 } from "ink";
2017
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1624
2018
  var globalBindings = [
1625
2019
  { key: "1-4", label: "Focus" },
1626
2020
  { key: "j/k", label: "Navigate" },
@@ -1631,20 +2025,24 @@ var modalBindings = [
1631
2025
  ];
1632
2026
  function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
1633
2027
  const allBindings = modalOpen ? [...contextBindings, ...modalBindings] : [...contextBindings, ...globalBindings];
1634
- return /* @__PURE__ */ jsx12(Box12, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
1635
- /* @__PURE__ */ jsx12(Text12, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
1636
- /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: binding.label })
2028
+ return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2029
+ /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2030
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
1637
2031
  ] }, binding.key)) });
1638
2032
  }
1639
2033
 
1640
2034
  // src/app.tsx
1641
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2035
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
1642
2036
  function App() {
1643
2037
  const { exit } = useApp();
1644
- const [focusedView, setFocusedView] = useState8("github");
1645
- const [modalOpen, setModalOpen] = useState8(false);
1646
- const [contextBindings, setContextBindings] = useState8([]);
1647
- useInput10(
2038
+ const [focusedView, setFocusedView] = useState9("github");
2039
+ const [modalOpen, setModalOpen] = useState9(false);
2040
+ const [contextBindings, setContextBindings] = useState9([]);
2041
+ const [logRefreshKey, setLogRefreshKey] = useState9(0);
2042
+ const handleLogUpdated = useCallback4(() => {
2043
+ setLogRefreshKey((prev) => prev + 1);
2044
+ }, []);
2045
+ useInput13(
1648
2046
  (input, key) => {
1649
2047
  if (key.ctrl && input === "c") {
1650
2048
  exit();
@@ -1655,26 +2053,43 @@ function App() {
1655
2053
  if (input === "4") {
1656
2054
  setFocusedView("jira");
1657
2055
  }
2056
+ if (input === "5" || input === "6") {
2057
+ setFocusedView("logs");
2058
+ }
1658
2059
  },
1659
2060
  { isActive: !modalOpen }
1660
2061
  );
1661
- return /* @__PURE__ */ jsxs13(Box13, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
1662
- /* @__PURE__ */ jsx13(
1663
- GitHubView,
1664
- {
1665
- isFocused: focusedView === "github",
1666
- onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0
1667
- }
1668
- ),
1669
- /* @__PURE__ */ jsx13(
1670
- JiraView,
1671
- {
1672
- isFocused: focusedView === "jira",
1673
- onModalChange: setModalOpen,
1674
- onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0
1675
- }
1676
- ),
1677
- /* @__PURE__ */ jsx13(KeybindingsBar, { contextBindings, modalOpen })
2062
+ return /* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
2063
+ /* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "row", columnGap: 1, children: [
2064
+ /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
2065
+ /* @__PURE__ */ jsx16(
2066
+ GitHubView,
2067
+ {
2068
+ isFocused: focusedView === "github",
2069
+ onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0,
2070
+ onLogUpdated: handleLogUpdated
2071
+ }
2072
+ ),
2073
+ /* @__PURE__ */ jsx16(
2074
+ JiraView,
2075
+ {
2076
+ isFocused: focusedView === "jira",
2077
+ onModalChange: setModalOpen,
2078
+ onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0,
2079
+ onLogUpdated: handleLogUpdated
2080
+ }
2081
+ )
2082
+ ] }),
2083
+ /* @__PURE__ */ jsx16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsx16(
2084
+ LogsView,
2085
+ {
2086
+ isFocused: focusedView === "logs",
2087
+ onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
2088
+ refreshKey: logRefreshKey
2089
+ }
2090
+ ) })
2091
+ ] }),
2092
+ /* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings, modalOpen })
1678
2093
  ] });
1679
2094
  }
1680
2095
 
@@ -1682,34 +2097,34 @@ function App() {
1682
2097
  import { render as inkRender } from "ink";
1683
2098
 
1684
2099
  // src/lib/Screen.tsx
1685
- import { Box as Box14, useStdout } from "ink";
1686
- import { useCallback as useCallback3, useEffect as useEffect6, useState as useState9 } from "react";
1687
- import { jsx as jsx14 } from "react/jsx-runtime";
2100
+ import { Box as Box17, useStdout } from "ink";
2101
+ import { useCallback as useCallback5, useEffect as useEffect7, useState as useState10 } from "react";
2102
+ import { jsx as jsx17 } from "react/jsx-runtime";
1688
2103
  function Screen({ children }) {
1689
2104
  const { stdout } = useStdout();
1690
- const getSize = useCallback3(
2105
+ const getSize = useCallback5(
1691
2106
  () => ({ height: stdout.rows, width: stdout.columns }),
1692
2107
  [stdout]
1693
2108
  );
1694
- const [size, setSize] = useState9(getSize);
1695
- useEffect6(() => {
2109
+ const [size, setSize] = useState10(getSize);
2110
+ useEffect7(() => {
1696
2111
  const onResize = () => setSize(getSize());
1697
2112
  stdout.on("resize", onResize);
1698
2113
  return () => {
1699
2114
  stdout.off("resize", onResize);
1700
2115
  };
1701
2116
  }, [stdout, getSize]);
1702
- return /* @__PURE__ */ jsx14(Box14, { height: size.height, width: size.width, children });
2117
+ return /* @__PURE__ */ jsx17(Box17, { height: size.height, width: size.width, children });
1703
2118
  }
1704
2119
 
1705
2120
  // src/lib/render.tsx
1706
- import { jsx as jsx15 } from "react/jsx-runtime";
2121
+ import { jsx as jsx18 } from "react/jsx-runtime";
1707
2122
  var ENTER_ALT_BUFFER = "\x1B[?1049h";
1708
2123
  var EXIT_ALT_BUFFER = "\x1B[?1049l";
1709
2124
  var CLEAR_SCREEN = "\x1B[2J\x1B[H";
1710
2125
  function render(node, options) {
1711
2126
  process.stdout.write(ENTER_ALT_BUFFER + CLEAR_SCREEN);
1712
- const element = /* @__PURE__ */ jsx15(Screen, { children: node });
2127
+ const element = /* @__PURE__ */ jsx18(Screen, { children: node });
1713
2128
  const instance = inkRender(element, options);
1714
2129
  setImmediate(() => instance.rerender(element));
1715
2130
  const cleanup = () => process.stdout.write(EXIT_ALT_BUFFER);
@@ -1730,7 +2145,7 @@ function render(node, options) {
1730
2145
  }
1731
2146
 
1732
2147
  // src/cli.tsx
1733
- import { jsx as jsx16 } from "react/jsx-runtime";
2148
+ import { jsx as jsx19 } from "react/jsx-runtime";
1734
2149
  meow(
1735
2150
  `
1736
2151
  Usage
@@ -1752,4 +2167,4 @@ meow(
1752
2167
  }
1753
2168
  }
1754
2169
  );
1755
- render(/* @__PURE__ */ jsx16(App, {}));
2170
+ render(/* @__PURE__ */ jsx19(App, {}));