clairo 0.4.0 → 0.5.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 +1200 -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,965 @@ 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, 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}: ${oldStatus} \u2192 ${newStatus}
582
+
583
+ `;
584
+ appendToLog(today, entry);
616
585
  }
617
586
 
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 }));
587
+ // src/components/github/PRDetailsBox.tsx
588
+ import { useRef } from "react";
589
+ import open from "open";
590
+ import { TitledBox } from "@mishieck/ink-titled-box";
591
+ import { Box as Box2, Text as Text2, useInput } from "ink";
592
+ import { ScrollView } from "ink-scroll-view";
593
+
594
+ // src/components/ui/Markdown.tsx
595
+ import { Box, Text } from "ink";
596
+ import Link from "ink-link";
597
+ import { marked } from "marked";
598
+ import Table from "cli-table3";
599
+ import { jsx, jsxs } from "react/jsx-runtime";
600
+ function Markdown({ children }) {
601
+ const tokens = marked.lexer(children);
602
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: tokens.map((token, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token }, idx)) });
603
+ }
604
+ function TokenRenderer({ token }) {
605
+ var _a, _b;
606
+ switch (token.type) {
607
+ case "heading":
608
+ return /* @__PURE__ */ jsx(Box, { marginTop: token.depth === 1 ? 0 : 1, children: /* @__PURE__ */ jsx(Text, { bold: true, underline: token.depth === 1, children: renderInline(token.tokens) }) });
609
+ case "paragraph": {
610
+ const hasLinks = (_a = token.tokens) == null ? void 0 : _a.some((t) => {
611
+ var _a2;
612
+ return t.type === "link" || t.type === "strong" && "tokens" in t && ((_a2 = t.tokens) == null ? void 0 : _a2.some((st) => st.type === "link"));
613
+ });
614
+ if (hasLinks) {
615
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: renderInline(token.tokens) });
696
616
  }
697
- } catch (err) {
698
- setErrors((prev) => ({ ...prev, prs: String(err) }));
699
- } finally {
700
- setLoading((prev) => ({ ...prev, prs: false }));
617
+ return /* @__PURE__ */ jsx(Text, { children: renderInline(token.tokens) });
701
618
  }
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 }));
619
+ case "code":
620
+ return /* @__PURE__ */ jsx(Box, { marginY: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: token.text }) });
621
+ case "blockquote":
622
+ return /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
623
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2502 " }),
624
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: (_b = token.tokens) == null ? void 0 : _b.map((t, idx) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, idx)) })
625
+ ] });
626
+ case "list":
627
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, children: token.items.map((item, idx) => /* @__PURE__ */ jsxs(Box, { children: [
628
+ /* @__PURE__ */ jsx(Text, { children: token.ordered ? `${idx + 1}. ` : "\u2022 " }),
629
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: item.tokens.map((t, i) => /* @__PURE__ */ jsx(TokenRenderer, { token: t }, i)) })
630
+ ] }, idx)) });
631
+ case "table":
632
+ return /* @__PURE__ */ jsx(TableRenderer, { token });
633
+ case "hr":
634
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(40) });
635
+ case "space":
636
+ return null;
637
+ default:
638
+ if ("text" in token && typeof token.text === "string") {
639
+ return /* @__PURE__ */ jsx(Text, { children: token.text });
713
640
  }
714
- } catch (err) {
715
- setErrors((prev) => ({ ...prev, details: String(err) }));
716
- } finally {
717
- setLoading((prev) => ({ ...prev, details: false }));
641
+ return null;
642
+ }
643
+ }
644
+ function TableRenderer({ token }) {
645
+ const table = new Table({
646
+ head: token.header.map((cell) => renderInlineToString(cell.tokens)),
647
+ style: { head: ["cyan"], border: ["gray"] }
648
+ });
649
+ for (const row of token.rows) {
650
+ table.push(row.map((cell) => renderInlineToString(cell.tokens)));
651
+ }
652
+ return /* @__PURE__ */ jsx(Text, { children: table.toString() });
653
+ }
654
+ function renderInline(tokens) {
655
+ if (!tokens) return null;
656
+ return tokens.map((token, idx) => {
657
+ switch (token.type) {
658
+ case "text":
659
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
660
+ case "strong":
661
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: renderInline(token.tokens) }, idx);
662
+ case "em":
663
+ return /* @__PURE__ */ jsx(Text, { italic: true, children: renderInline(token.tokens) }, idx);
664
+ case "codespan":
665
+ return /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
666
+ "`",
667
+ token.text,
668
+ "`"
669
+ ] }, idx);
670
+ case "link":
671
+ return /* @__PURE__ */ jsx(Link, { url: token.href, children: /* @__PURE__ */ jsx(Text, { color: "blue", children: renderInlineToString(token.tokens) }) }, idx);
672
+ case "image":
673
+ return /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
674
+ "[Image: ",
675
+ token.text || token.href,
676
+ "]"
677
+ ] }, idx);
678
+ case "br":
679
+ return /* @__PURE__ */ jsx(Text, { children: "\n" }, idx);
680
+ case "del":
681
+ return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: renderInline(token.tokens) }, idx);
682
+ default:
683
+ if ("text" in token && typeof token.text === "string") {
684
+ return /* @__PURE__ */ jsx(Text, { children: token.text }, idx);
685
+ }
686
+ return null;
718
687
  }
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();
688
+ });
689
+ }
690
+ function renderInlineToString(tokens) {
691
+ if (!tokens) return "";
692
+ return tokens.map((token) => {
693
+ if ("text" in token && typeof token.text === "string") {
694
+ return token.text;
733
695
  }
734
- }, [currentRepoSlug, currentBranch, refreshPRs]);
735
- useEffect3(() => {
736
- if (!selectedPR || !currentRepoSlug) {
737
- setPrDetails(null);
738
- return;
696
+ if ("tokens" in token && Array.isArray(token.tokens)) {
697
+ return renderInlineToString(token.tokens);
739
698
  }
740
- refreshDetails();
741
- }, [selectedPR, currentRepoSlug, refreshDetails]);
742
- const handleRemoteSelect = useCallback(
743
- (remoteName) => {
744
- setSelectedRemote(remoteName);
745
- if (repoPath) {
746
- updateRepoConfig(repoPath, { selectedRemote: remoteName });
699
+ return "";
700
+ }).join("");
701
+ }
702
+
703
+ // src/components/github/PRDetailsBox.tsx
704
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
705
+ function getCheckColor(check) {
706
+ const conclusion = check.conclusion ?? check.state;
707
+ if (conclusion === "SUCCESS") return "green";
708
+ if (conclusion === "FAILURE" || conclusion === "ERROR") return "red";
709
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "gray";
710
+ if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
711
+ return "yellow";
712
+ if (check.status === "COMPLETED") return "green";
713
+ return void 0;
714
+ }
715
+ function getCheckIcon(check) {
716
+ const conclusion = check.conclusion ?? check.state;
717
+ if (conclusion === "SUCCESS") return "\u2713";
718
+ if (conclusion === "FAILURE" || conclusion === "ERROR") return "\u2717";
719
+ if (conclusion === "SKIPPED" || conclusion === "NEUTRAL") return "\u25CB";
720
+ if (conclusion === "PENDING" || check.status === "IN_PROGRESS" || check.status === "QUEUED" || check.status === "WAITING")
721
+ return "\u25CF";
722
+ if (check.status === "COMPLETED") return "\u2713";
723
+ return "?";
724
+ }
725
+ function PRDetailsBox({ pr, loading, error, isFocused }) {
726
+ var _a, _b, _c, _d, _e, _f, _g;
727
+ const scrollRef = useRef(null);
728
+ const title = "[3] PR Details";
729
+ const borderColor = isFocused ? "yellow" : void 0;
730
+ const displayTitle = pr ? `${title} - #${pr.number}` : title;
731
+ const reviewStatus = (pr == null ? void 0 : pr.reviewDecision) ?? "PENDING";
732
+ const reviewColor = reviewStatus === "APPROVED" ? "green" : reviewStatus === "CHANGES_REQUESTED" ? "red" : "yellow";
733
+ const getMergeDisplay = () => {
734
+ if (!pr) return { text: "UNKNOWN", color: "yellow" };
735
+ if (pr.state === "MERGED") return { text: "MERGED", color: "magenta" };
736
+ if (pr.state === "CLOSED") return { text: "CLOSED", color: "red" };
737
+ if (pr.mergeable === "MERGEABLE") return { text: "MERGEABLE", color: "green" };
738
+ if (pr.mergeable === "CONFLICTING") return { text: "CONFLICTING", color: "red" };
739
+ return { text: pr.mergeable ?? "UNKNOWN", color: "yellow" };
740
+ };
741
+ const mergeDisplay = getMergeDisplay();
742
+ useInput(
743
+ (input, key) => {
744
+ var _a2, _b2;
745
+ if (key.upArrow || input === "k") {
746
+ (_a2 = scrollRef.current) == null ? void 0 : _a2.scrollBy(-1);
747
747
  }
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();
748
+ if (key.downArrow || input === "j") {
749
+ (_b2 = scrollRef.current) == null ? void 0 : _b2.scrollBy(1);
750
+ }
751
+ if (input === "o" && (pr == null ? void 0 : pr.url)) {
752
+ open(pr.url).catch(() => {
753
+ });
767
754
  }
768
755
  },
769
756
  { isActive: isFocused }
770
757
  );
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
- ] });
758
+ 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: [
759
+ loading && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Loading details..." }),
760
+ error && /* @__PURE__ */ jsx2(Text2, { color: "red", children: error }),
761
+ !loading && !error && !pr && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Select a PR to view details" }),
762
+ !loading && !error && pr && /* @__PURE__ */ jsxs2(Fragment, { children: [
763
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: pr.title }),
764
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
765
+ "by ",
766
+ ((_a = pr.author) == null ? void 0 : _a.login) ?? "unknown",
767
+ " | ",
768
+ ((_b = pr.commits) == null ? void 0 : _b.length) ?? 0,
769
+ " commits"
770
+ ] }),
771
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
772
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Review: " }),
773
+ /* @__PURE__ */ jsx2(Text2, { color: reviewColor, children: reviewStatus }),
774
+ /* @__PURE__ */ jsx2(Text2, { children: " | " }),
775
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Status: " }),
776
+ /* @__PURE__ */ jsx2(Text2, { color: mergeDisplay.color, children: mergeDisplay.text })
777
+ ] }),
778
+ (((_c = pr.assignees) == null ? void 0 : _c.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
779
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Assignees: " }),
780
+ /* @__PURE__ */ jsx2(Text2, { children: pr.assignees.map((a) => a.login).join(", ") })
781
+ ] }),
782
+ (((_d = pr.reviews) == null ? void 0 : _d.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
783
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Reviews:" }),
784
+ pr.reviews.map((review, idx) => {
785
+ const color = review.state === "APPROVED" ? "green" : review.state === "CHANGES_REQUESTED" ? "red" : review.state === "COMMENTED" ? "blue" : "yellow";
786
+ const icon = review.state === "APPROVED" ? "\u2713" : review.state === "CHANGES_REQUESTED" ? "\u2717" : review.state === "COMMENTED" ? "\u{1F4AC}" : "\u25CB";
787
+ return /* @__PURE__ */ jsxs2(Text2, { color, children: [
788
+ " ",
789
+ icon,
790
+ " ",
791
+ review.author.login
792
+ ] }, idx);
793
+ })
794
+ ] }),
795
+ (((_e = pr.reviewRequests) == null ? void 0 : _e.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { children: [
796
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Pending: " }),
797
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: pr.reviewRequests.map((r) => r.login ?? r.name ?? r.slug ?? "Team").join(", ") })
798
+ ] }),
799
+ (((_f = pr.statusCheckRollup) == null ? void 0 : _f.length) ?? 0) > 0 && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
800
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Checks:" }),
801
+ (_g = pr.statusCheckRollup) == null ? void 0 : _g.map((check, idx) => /* @__PURE__ */ jsxs2(Text2, { color: getCheckColor(check), children: [
802
+ " ",
803
+ getCheckIcon(check),
804
+ " ",
805
+ check.name ?? check.context
806
+ ] }, idx))
807
+ ] }),
808
+ pr.body && /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
809
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Description:" }),
810
+ /* @__PURE__ */ jsx2(Markdown, { children: pr.body })
811
+ ] })
812
+ ] })
813
+ ] }) }) }) });
810
814
  }
811
815
 
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";
816
+ // src/components/github/PullRequestsBox.tsx
817
+ import { useEffect, useState } from "react";
818
+ import { TitledBox as TitledBox2 } from "@mishieck/ink-titled-box";
819
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
817
820
 
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
- }
821
+ // src/lib/clipboard.ts
822
+ import { exec as exec2 } from "child_process";
823
+ async function copyToClipboard(text) {
824
+ var _a, _b;
825
+ const command = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip -selection clipboard";
826
+ try {
827
+ const child = exec2(command);
828
+ (_a = child.stdin) == null ? void 0 : _a.write(text);
829
+ (_b = child.stdin) == null ? void 0 : _b.end();
830
+ await new Promise((resolve, reject) => {
831
+ child.on("close", (code) => {
832
+ if (code === 0) resolve();
833
+ else reject(new Error(`Clipboard command exited with code ${code}`));
834
+ });
835
+ });
836
+ return true;
837
+ } catch {
838
+ return false;
842
839
  }
843
- return null;
844
840
  }
845
841
 
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]) ?? [];
842
+ // src/components/github/PullRequestsBox.tsx
843
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
844
+ function PullRequestsBox({
845
+ prs,
846
+ selectedPR,
847
+ onSelect,
848
+ onCreatePR,
849
+ loading,
850
+ error,
851
+ branch,
852
+ repoSlug,
853
+ isFocused
854
+ }) {
855
+ const [highlightedIndex, setHighlightedIndex] = useState(0);
856
+ const totalItems = prs.length + 1;
857
+ useEffect(() => {
858
+ const idx = prs.findIndex((p) => p.number === (selectedPR == null ? void 0 : selectedPR.number));
859
+ if (idx >= 0) setHighlightedIndex(idx);
860
+ }, [selectedPR, prs]);
861
+ useInput2(
862
+ (input, key) => {
863
+ if (!isFocused) return;
864
+ if (key.upArrow || input === "k") {
865
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
866
+ }
867
+ if (key.downArrow || input === "j") {
868
+ setHighlightedIndex((prev) => Math.min(totalItems - 1, prev + 1));
869
+ }
870
+ if (key.return) {
871
+ if (highlightedIndex === prs.length) {
872
+ onCreatePR();
873
+ } else if (prs[highlightedIndex]) {
874
+ onSelect(prs[highlightedIndex]);
875
+ }
876
+ }
877
+ if (input === "y" && repoSlug && prs[highlightedIndex]) {
878
+ const pr = prs[highlightedIndex];
879
+ const url = `https://github.com/${repoSlug}/pull/${pr.number}`;
880
+ copyToClipboard(url);
881
+ }
882
+ },
883
+ { isActive: isFocused }
884
+ );
885
+ const title = "[2] Pull Requests";
886
+ const subtitle = branch ? ` (${branch})` : "";
887
+ const borderColor = isFocused ? "yellow" : void 0;
888
+ return /* @__PURE__ */ jsx3(TitledBox2, { borderStyle: "round", titles: [`${title}${subtitle}`], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
889
+ loading && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Loading PRs..." }),
890
+ error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
891
+ !loading && !error && /* @__PURE__ */ jsxs3(Fragment2, { children: [
892
+ prs.length === 0 && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No PRs for this branch" }),
893
+ prs.map((pr, idx) => {
894
+ const isHighlighted = isFocused && idx === highlightedIndex;
895
+ const isSelected = pr.number === (selectedPR == null ? void 0 : selectedPR.number);
896
+ const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
897
+ return /* @__PURE__ */ jsxs3(Text3, { color: isSelected ? "green" : void 0, children: [
898
+ prefix,
899
+ "#",
900
+ pr.number,
901
+ " ",
902
+ pr.isDraft ? "[Draft] " : "",
903
+ pr.title
904
+ ] }, pr.number);
905
+ }),
906
+ /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
907
+ isFocused && highlightedIndex === prs.length ? "> " : " ",
908
+ "+ Create new PR"
909
+ ] })
910
+ ] })
911
+ ] }) });
872
912
  }
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
- });
913
+
914
+ // src/components/github/RemotesBox.tsx
915
+ import { useEffect as useEffect2, useState as useState2 } from "react";
916
+ import { TitledBox as TitledBox3 } from "@mishieck/ink-titled-box";
917
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
918
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
919
+ function RemotesBox({ remotes, selectedRemote, onSelect, loading, error, isFocused }) {
920
+ const [highlightedIndex, setHighlightedIndex] = useState2(0);
921
+ useEffect2(() => {
922
+ const idx = remotes.findIndex((r) => r.name === selectedRemote);
923
+ if (idx >= 0) setHighlightedIndex(idx);
924
+ }, [selectedRemote, remotes]);
925
+ useInput3(
926
+ (input, key) => {
927
+ if (!isFocused || remotes.length === 0) return;
928
+ if (key.upArrow || input === "k") {
929
+ setHighlightedIndex((prev) => Math.max(0, prev - 1));
930
+ }
931
+ if (key.downArrow || input === "j") {
932
+ setHighlightedIndex((prev) => Math.min(remotes.length - 1, prev + 1));
933
+ }
934
+ if (key.return) {
935
+ onSelect(remotes[highlightedIndex].name);
936
+ }
937
+ },
938
+ { isActive: isFocused }
939
+ );
940
+ const title = "[1] Remotes";
941
+ const borderColor = isFocused ? "yellow" : void 0;
942
+ return /* @__PURE__ */ jsx4(TitledBox3, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [
943
+ loading && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Loading..." }),
944
+ error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error }),
945
+ !loading && !error && remotes.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "No remotes configured" }),
946
+ !loading && !error && remotes.map((remote, idx) => {
947
+ const isHighlighted = isFocused && idx === highlightedIndex;
948
+ const isSelected = remote.name === selectedRemote;
949
+ const prefix = isHighlighted ? "> " : isSelected ? "\u25CF " : " ";
950
+ return /* @__PURE__ */ jsxs4(Text4, { color: isSelected ? "green" : void 0, children: [
951
+ prefix,
952
+ remote.name,
953
+ " (",
954
+ remote.url,
955
+ ")"
956
+ ] }, remote.name);
957
+ })
958
+ ] }) });
886
959
  }
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
- }
960
+
961
+ // src/components/github/GitHubView.tsx
962
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
963
+ function GitHubView({ isFocused, onKeybindingsChange, onLogUpdated }) {
964
+ const [isRepo, setIsRepo] = useState3(null);
965
+ const [repoPath, setRepoPath] = useState3(null);
966
+ const [remotes, setRemotes] = useState3([]);
967
+ const [currentBranch, setCurrentBranch] = useState3(null);
968
+ const [currentRepoSlug, setCurrentRepoSlug] = useState3(null);
969
+ const [selectedRemote, setSelectedRemote] = useState3(null);
970
+ const [selectedPR, setSelectedPR] = useState3(null);
971
+ const [prs, setPrs] = useState3([]);
972
+ const [prDetails, setPrDetails] = useState3(null);
973
+ const [loading, setLoading] = useState3({
974
+ remotes: true,
975
+ prs: false,
976
+ details: false
896
977
  });
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)
978
+ const [errors, setErrors] = useState3({});
979
+ const [focusedBox, setFocusedBox] = useState3("remotes");
980
+ useEffect3(() => {
981
+ if (!isFocused) {
982
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
983
+ return;
906
984
  }
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);
985
+ const bindings = [];
986
+ if (focusedBox === "remotes") {
987
+ bindings.push({ key: "Enter", label: "Select Remote" });
988
+ } else if (focusedBox === "prs") {
989
+ bindings.push({ key: "n", label: "New PR", color: "green" });
990
+ bindings.push({ key: "r", label: "Refresh" });
991
+ bindings.push({ key: "o", label: "Open", color: "green" });
992
+ bindings.push({ key: "y", label: "Copy Link" });
993
+ } else if (focusedBox === "details") {
994
+ bindings.push({ key: "r", label: "Refresh" });
995
+ bindings.push({ key: "o", label: "Open", color: "green" });
927
996
  }
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 };
997
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
998
+ }, [isFocused, focusedBox, onKeybindingsChange]);
999
+ useEffect3(() => {
1000
+ const gitRepoCheck = isGitRepo();
1001
+ setIsRepo(gitRepoCheck);
1002
+ if (!gitRepoCheck) {
1003
+ setLoading((prev) => ({ ...prev, remotes: false }));
1004
+ setErrors((prev) => ({ ...prev, remotes: "Not a git repository" }));
1005
+ return;
932
1006
  }
933
- if (response.status === 204) {
934
- return { ok: true, status: response.status, data: null };
1007
+ const rootResult = getRepoRoot();
1008
+ if (rootResult.success) {
1009
+ setRepoPath(rootResult.data);
935
1010
  }
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
- };
1011
+ const branchResult = getCurrentBranch();
1012
+ if (branchResult.success) {
1013
+ setCurrentBranch(branchResult.data);
952
1014
  }
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
- };
1015
+ const remotesResult = listRemotes();
1016
+ if (remotesResult.success) {
1017
+ setRemotes(remotesResult.data);
1018
+ const remoteNames = remotesResult.data.map((r) => r.name);
1019
+ const defaultRemote = getSelectedRemote(rootResult.success ? rootResult.data : "", remoteNames);
1020
+ setSelectedRemote(defaultRemote);
1021
+ } else {
1022
+ setErrors((prev) => ({ ...prev, remotes: remotesResult.error }));
970
1023
  }
971
- if (result.status === 404) {
972
- return {
973
- success: false,
974
- error: `Ticket ${ticketKey} not found`,
975
- errorType: "invalid_ticket"
976
- };
1024
+ setLoading((prev) => ({ ...prev, remotes: false }));
1025
+ }, []);
1026
+ const refreshPRs = useCallback(async () => {
1027
+ if (!currentBranch || !currentRepoSlug) return;
1028
+ setLoading((prev) => ({ ...prev, prs: true }));
1029
+ try {
1030
+ const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1031
+ if (result.success) {
1032
+ setPrs(result.data);
1033
+ if (result.data.length > 0) {
1034
+ setSelectedPR((prev) => prev ?? result.data[0]);
1035
+ }
1036
+ setErrors((prev) => ({ ...prev, prs: void 0 }));
1037
+ } else {
1038
+ setErrors((prev) => ({ ...prev, prs: result.error }));
1039
+ }
1040
+ } catch (err) {
1041
+ setErrors((prev) => ({ ...prev, prs: String(err) }));
1042
+ } finally {
1043
+ setLoading((prev) => ({ ...prev, prs: false }));
977
1044
  }
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
- };
1045
+ }, [currentBranch, currentRepoSlug]);
1046
+ const refreshDetails = useCallback(async () => {
1047
+ if (!selectedPR || !currentRepoSlug) return;
1048
+ setLoading((prev) => ({ ...prev, details: true }));
1049
+ try {
1050
+ const result = await getPRDetails(selectedPR.number, currentRepoSlug);
1051
+ if (result.success) {
1052
+ setPrDetails(result.data);
1053
+ setErrors((prev) => ({ ...prev, details: void 0 }));
1054
+ } else {
1055
+ setErrors((prev) => ({ ...prev, details: result.error }));
1056
+ }
1057
+ } catch (err) {
1058
+ setErrors((prev) => ({ ...prev, details: String(err) }));
1059
+ } finally {
1060
+ setLoading((prev) => ({ ...prev, details: false }));
995
1061
  }
996
- if (result.status === 404) {
997
- return {
998
- success: false,
999
- error: `Ticket ${ticketKey} not found`,
1000
- errorType: "invalid_ticket"
1001
- };
1062
+ }, [selectedPR, currentRepoSlug]);
1063
+ useEffect3(() => {
1064
+ if (!selectedRemote || !currentBranch) return;
1065
+ const remote = remotes.find((r) => r.name === selectedRemote);
1066
+ if (!remote) return;
1067
+ const repo = getRepoFromRemote(remote.url);
1068
+ if (!repo) return;
1069
+ setCurrentRepoSlug(repo);
1070
+ setPrs([]);
1071
+ setSelectedPR(null);
1072
+ }, [selectedRemote, currentBranch, remotes]);
1073
+ useEffect3(() => {
1074
+ if (currentRepoSlug && currentBranch) {
1075
+ refreshPRs();
1002
1076
  }
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
- };
1077
+ }, [currentRepoSlug, currentBranch, refreshPRs]);
1078
+ useEffect3(() => {
1079
+ if (!selectedPR || !currentRepoSlug) {
1080
+ setPrDetails(null);
1081
+ return;
1024
1082
  }
1025
- if (result.status === 404) {
1026
- return {
1027
- success: false,
1028
- error: `Ticket ${ticketKey} not found`,
1029
- errorType: "invalid_ticket"
1030
- };
1083
+ refreshDetails();
1084
+ }, [selectedPR, currentRepoSlug, refreshDetails]);
1085
+ const handleRemoteSelect = useCallback(
1086
+ (remoteName) => {
1087
+ setSelectedRemote(remoteName);
1088
+ if (repoPath) {
1089
+ updateRepoConfig(repoPath, { selectedRemote: remoteName });
1090
+ }
1091
+ },
1092
+ [repoPath]
1093
+ );
1094
+ const handlePRSelect = useCallback((pr) => {
1095
+ setSelectedPR(pr);
1096
+ }, []);
1097
+ const prNumbersBeforeCreate = useRef2(/* @__PURE__ */ new Set());
1098
+ const pollingIntervalRef = useRef2(null);
1099
+ const handleCreatePR = useCallback(() => {
1100
+ prNumbersBeforeCreate.current = new Set(prs.map((pr) => pr.number));
1101
+ exec3("gh pr create --web", () => {
1102
+ process.stdout.emit("resize");
1103
+ });
1104
+ if (!currentBranch || !currentRepoSlug) return;
1105
+ let attempts = 0;
1106
+ const maxAttempts = 24;
1107
+ const pollInterval = 5e3;
1108
+ if (pollingIntervalRef.current) {
1109
+ clearInterval(pollingIntervalRef.current);
1031
1110
  }
1032
- return {
1033
- success: false,
1034
- error: result.error ?? "Failed to apply transition",
1035
- errorType: "api_error"
1111
+ pollingIntervalRef.current = setInterval(async () => {
1112
+ attempts++;
1113
+ if (attempts > maxAttempts) {
1114
+ if (pollingIntervalRef.current) {
1115
+ clearInterval(pollingIntervalRef.current);
1116
+ pollingIntervalRef.current = null;
1117
+ }
1118
+ return;
1119
+ }
1120
+ const result = await listPRsForBranch(currentBranch, currentRepoSlug);
1121
+ if (result.success) {
1122
+ setPrs(result.data);
1123
+ const newPR = result.data.find((pr) => !prNumbersBeforeCreate.current.has(pr.number));
1124
+ if (newPR) {
1125
+ if (pollingIntervalRef.current) {
1126
+ clearInterval(pollingIntervalRef.current);
1127
+ pollingIntervalRef.current = null;
1128
+ }
1129
+ const tickets = repoPath && currentBranch ? getLinkedTickets(repoPath, currentBranch).map((t) => t.key) : [];
1130
+ logPRCreated(newPR.number, newPR.title, tickets);
1131
+ onLogUpdated == null ? void 0 : onLogUpdated();
1132
+ setSelectedPR(newPR);
1133
+ }
1134
+ }
1135
+ }, pollInterval);
1136
+ }, [prs, currentBranch, currentRepoSlug, repoPath, onLogUpdated]);
1137
+ useEffect3(() => {
1138
+ return () => {
1139
+ if (pollingIntervalRef.current) {
1140
+ clearInterval(pollingIntervalRef.current);
1141
+ }
1036
1142
  };
1143
+ }, []);
1144
+ useInput4(
1145
+ (input) => {
1146
+ if (input === "1") setFocusedBox("remotes");
1147
+ if (input === "2") setFocusedBox("prs");
1148
+ if (input === "3") setFocusedBox("details");
1149
+ if (input === "r") {
1150
+ if (focusedBox === "prs") refreshPRs();
1151
+ if (focusedBox === "details") refreshDetails();
1152
+ }
1153
+ },
1154
+ { isActive: isFocused }
1155
+ );
1156
+ if (isRepo === false) {
1157
+ 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
1158
  }
1038
- return { success: true, data: null };
1159
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
1160
+ /* @__PURE__ */ jsx5(
1161
+ RemotesBox,
1162
+ {
1163
+ remotes,
1164
+ selectedRemote,
1165
+ onSelect: handleRemoteSelect,
1166
+ loading: loading.remotes,
1167
+ error: errors.remotes,
1168
+ isFocused: isFocused && focusedBox === "remotes"
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ jsx5(
1172
+ PullRequestsBox,
1173
+ {
1174
+ prs,
1175
+ selectedPR,
1176
+ onSelect: handlePRSelect,
1177
+ onCreatePR: handleCreatePR,
1178
+ loading: loading.prs,
1179
+ error: errors.prs,
1180
+ branch: currentBranch,
1181
+ repoSlug: currentRepoSlug,
1182
+ isFocused: isFocused && focusedBox === "prs"
1183
+ }
1184
+ ),
1185
+ /* @__PURE__ */ jsx5(
1186
+ PRDetailsBox,
1187
+ {
1188
+ pr: prDetails,
1189
+ loading: loading.details,
1190
+ error: errors.details,
1191
+ isFocused: isFocused && focusedBox === "details"
1192
+ }
1193
+ )
1194
+ ] });
1039
1195
  }
1040
1196
 
1197
+ // src/components/jira/JiraView.tsx
1198
+ import { useCallback as useCallback2, useEffect as useEffect5, useState as useState7 } from "react";
1199
+ import open2 from "open";
1200
+ import { TitledBox as TitledBox5 } from "@mishieck/ink-titled-box";
1201
+ import { Box as Box11, Text as Text11, useInput as useInput9 } from "ink";
1202
+
1041
1203
  // src/components/jira/ChangeStatusModal.tsx
1042
1204
  import { useEffect as useEffect4, useState as useState4 } from "react";
1043
1205
  import { Box as Box6, Text as Text6, useInput as useInput5 } from "ink";
@@ -1121,17 +1283,17 @@ import { useState as useState5 } from "react";
1121
1283
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
1122
1284
 
1123
1285
  // src/lib/editor.ts
1124
- import { spawnSync } from "child_process";
1125
- import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1286
+ import { spawnSync as spawnSync2 } from "child_process";
1287
+ import { mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
1126
1288
  import { tmpdir } from "os";
1127
- import { join as join2 } from "path";
1289
+ import { join as join3 } from "path";
1128
1290
  function openInEditor(content, filename) {
1129
1291
  const editor = process.env.VISUAL || process.env.EDITOR || "vi";
1130
- const tempDir = mkdtempSync(join2(tmpdir(), "clairo-"));
1131
- const tempFile = join2(tempDir, filename);
1292
+ const tempDir = mkdtempSync(join3(tmpdir(), "clairo-"));
1293
+ const tempFile = join3(tempDir, filename);
1132
1294
  try {
1133
- writeFileSync2(tempFile, content);
1134
- const result = spawnSync(editor, [tempFile], {
1295
+ writeFileSync3(tempFile, content);
1296
+ const result = spawnSync2(editor, [tempFile], {
1135
1297
  stdio: "inherit"
1136
1298
  });
1137
1299
  process.stdout.write("\x1B[2J\x1B[H");
@@ -1139,7 +1301,7 @@ function openInEditor(content, filename) {
1139
1301
  if (result.status !== 0) {
1140
1302
  return null;
1141
1303
  }
1142
- return readFileSync2(tempFile, "utf-8");
1304
+ return readFileSync3(tempFile, "utf-8");
1143
1305
  } finally {
1144
1306
  try {
1145
1307
  rmSync(tempDir, { recursive: true });
@@ -1335,7 +1497,7 @@ function TicketItem({ ticketKey, summary, status, isHighlighted, isSelected }) {
1335
1497
 
1336
1498
  // src/components/jira/JiraView.tsx
1337
1499
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1338
- function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1500
+ function JiraView({ isFocused, onModalChange, onKeybindingsChange, onLogUpdated }) {
1339
1501
  const [repoPath, setRepoPath] = useState7(null);
1340
1502
  const [currentBranch, setCurrentBranch] = useState7(null);
1341
1503
  const [isRepo, setIsRepo] = useState7(null);
@@ -1592,7 +1754,10 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1592
1754
  ticketKey: ticket.key,
1593
1755
  currentStatus: ticket.status,
1594
1756
  onComplete: (newStatus) => {
1757
+ const oldStatus = ticket.status;
1595
1758
  updateTicketStatus(repoPath, currentBranch, ticket.key, newStatus);
1759
+ logJiraStatusChanged(ticket.key, oldStatus, newStatus);
1760
+ onLogUpdated == null ? void 0 : onLogUpdated();
1596
1761
  setShowStatusModal(false);
1597
1762
  refreshTickets();
1598
1763
  },
@@ -1618,9 +1783,237 @@ function JiraView({ isFocused, onModalChange, onKeybindingsChange }) {
1618
1783
  ] }) });
1619
1784
  }
1620
1785
 
1621
- // src/components/ui/KeybindingsBar.tsx
1622
- import { Box as Box12, Text as Text12 } from "ink";
1786
+ // src/components/logs/LogsView.tsx
1787
+ import { useCallback as useCallback3, useEffect as useEffect6, useState as useState8 } from "react";
1788
+ import { Box as Box14, useInput as useInput12 } from "ink";
1789
+
1790
+ // src/components/logs/LogsHistoryBox.tsx
1791
+ import { TitledBox as TitledBox6 } from "@mishieck/ink-titled-box";
1792
+ import { Box as Box12, Text as Text12, useInput as useInput10 } from "ink";
1623
1793
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1794
+ function LogsHistoryBox({
1795
+ logFiles,
1796
+ selectedDate,
1797
+ highlightedIndex,
1798
+ onHighlight,
1799
+ onSelect,
1800
+ isFocused
1801
+ }) {
1802
+ const title = "[5] Logs";
1803
+ const borderColor = isFocused ? "yellow" : void 0;
1804
+ useInput10(
1805
+ (input, key) => {
1806
+ if (logFiles.length === 0) return;
1807
+ if (key.upArrow || input === "k") {
1808
+ onHighlight(Math.max(0, highlightedIndex - 1));
1809
+ }
1810
+ if (key.downArrow || input === "j") {
1811
+ onHighlight(Math.min(logFiles.length - 1, highlightedIndex + 1));
1812
+ }
1813
+ if (key.return) {
1814
+ const file = logFiles[highlightedIndex];
1815
+ if (file) {
1816
+ onSelect(file.date);
1817
+ }
1818
+ }
1819
+ },
1820
+ { isActive: isFocused }
1821
+ );
1822
+ return /* @__PURE__ */ jsx12(TitledBox6, { borderStyle: "round", titles: [title], borderColor, flexShrink: 0, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, children: [
1823
+ logFiles.length === 0 && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "No logs yet" }),
1824
+ logFiles.map((file, idx) => {
1825
+ const isHighlighted = idx === highlightedIndex;
1826
+ const isSelected = file.date === selectedDate;
1827
+ const cursor = isHighlighted ? ">" : " ";
1828
+ const indicator = isSelected ? " *" : "";
1829
+ return /* @__PURE__ */ jsxs12(Box12, { children: [
1830
+ /* @__PURE__ */ jsxs12(Text12, { color: isHighlighted ? "yellow" : void 0, children: [
1831
+ cursor,
1832
+ " "
1833
+ ] }),
1834
+ /* @__PURE__ */ jsx12(
1835
+ Text12,
1836
+ {
1837
+ color: file.isToday ? "green" : void 0,
1838
+ bold: file.isToday,
1839
+ children: file.date
1840
+ }
1841
+ ),
1842
+ file.isToday && /* @__PURE__ */ jsx12(Text12, { color: "green", children: " (today)" }),
1843
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: indicator })
1844
+ ] }, file.date);
1845
+ })
1846
+ ] }) });
1847
+ }
1848
+
1849
+ // src/components/logs/LogViewerBox.tsx
1850
+ import { useRef as useRef3 } from "react";
1851
+ import { TitledBox as TitledBox7 } from "@mishieck/ink-titled-box";
1852
+ import { Box as Box13, Text as Text13, useInput as useInput11 } from "ink";
1853
+ import { ScrollView as ScrollView2 } from "ink-scroll-view";
1854
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1855
+ function LogViewerBox({ date, content, isFocused, onRefresh, onLogCreated }) {
1856
+ const scrollRef = useRef3(null);
1857
+ const title = "[6] Log Content";
1858
+ const borderColor = isFocused ? "yellow" : void 0;
1859
+ const displayTitle = date ? `${title} - ${date}.md` : title;
1860
+ useInput11(
1861
+ (input, key) => {
1862
+ var _a, _b;
1863
+ if (key.upArrow || input === "k") {
1864
+ (_a = scrollRef.current) == null ? void 0 : _a.scrollBy(-1);
1865
+ }
1866
+ if (key.downArrow || input === "j") {
1867
+ (_b = scrollRef.current) == null ? void 0 : _b.scrollBy(1);
1868
+ }
1869
+ if (input === "e" && date) {
1870
+ openLogInEditor(date);
1871
+ onRefresh();
1872
+ }
1873
+ if (input === "n") {
1874
+ const today = getTodayDate();
1875
+ if (!logExists(today)) {
1876
+ createEmptyLog(today);
1877
+ onLogCreated();
1878
+ }
1879
+ }
1880
+ if (input === "r") {
1881
+ onRefresh();
1882
+ }
1883
+ },
1884
+ { isActive: isFocused }
1885
+ );
1886
+ 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: [
1887
+ !date && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Select a log file to view" }),
1888
+ date && content === null && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Log file not found" }),
1889
+ date && content !== null && content.trim() === "" && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Empty log file" }),
1890
+ date && content && content.trim() !== "" && /* @__PURE__ */ jsx13(Markdown, { children: content })
1891
+ ] }) }) }) });
1892
+ }
1893
+
1894
+ // src/components/logs/LogsView.tsx
1895
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1896
+ function LogsView({ isFocused, onKeybindingsChange, refreshKey }) {
1897
+ const [logFiles, setLogFiles] = useState8([]);
1898
+ const [selectedDate, setSelectedDate] = useState8(null);
1899
+ const [logContent, setLogContent] = useState8(null);
1900
+ const [highlightedIndex, setHighlightedIndex] = useState8(0);
1901
+ const [focusedBox, setFocusedBox] = useState8("history");
1902
+ useEffect6(() => {
1903
+ if (!isFocused) {
1904
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange([]);
1905
+ return;
1906
+ }
1907
+ const bindings = [];
1908
+ if (focusedBox === "history") {
1909
+ bindings.push({ key: "Enter", label: "Select" });
1910
+ } else if (focusedBox === "viewer") {
1911
+ bindings.push({ key: "e", label: "Edit" });
1912
+ bindings.push({ key: "n", label: "New Log", color: "green" });
1913
+ bindings.push({ key: "r", label: "Refresh" });
1914
+ }
1915
+ onKeybindingsChange == null ? void 0 : onKeybindingsChange(bindings);
1916
+ }, [isFocused, focusedBox, onKeybindingsChange]);
1917
+ const refreshLogFiles = useCallback3(() => {
1918
+ const files = listLogFiles();
1919
+ setLogFiles(files);
1920
+ if (files.length > 0 && !selectedDate) {
1921
+ const today = getTodayDate();
1922
+ const todayFile = files.find((f) => f.date === today);
1923
+ if (todayFile) {
1924
+ setSelectedDate(todayFile.date);
1925
+ const idx = files.findIndex((f) => f.date === today);
1926
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1927
+ } else {
1928
+ setSelectedDate(files[0].date);
1929
+ setHighlightedIndex(0);
1930
+ }
1931
+ }
1932
+ }, [selectedDate]);
1933
+ useEffect6(() => {
1934
+ refreshLogFiles();
1935
+ }, [refreshLogFiles]);
1936
+ useEffect6(() => {
1937
+ if (selectedDate) {
1938
+ const content = readLog(selectedDate);
1939
+ setLogContent(content);
1940
+ } else {
1941
+ setLogContent(null);
1942
+ }
1943
+ }, [selectedDate]);
1944
+ useEffect6(() => {
1945
+ if (refreshKey !== void 0 && refreshKey > 0) {
1946
+ const files = listLogFiles();
1947
+ setLogFiles(files);
1948
+ const today = getTodayDate();
1949
+ if (selectedDate === today) {
1950
+ const content = readLog(today);
1951
+ setLogContent(content);
1952
+ } else if (!selectedDate && files.length > 0) {
1953
+ const todayFile = files.find((f) => f.date === today);
1954
+ if (todayFile) {
1955
+ setSelectedDate(today);
1956
+ const idx = files.findIndex((f) => f.date === today);
1957
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1958
+ }
1959
+ }
1960
+ }
1961
+ }, [refreshKey, selectedDate]);
1962
+ const handleSelectDate = useCallback3((date) => {
1963
+ setSelectedDate(date);
1964
+ }, []);
1965
+ const handleRefresh = useCallback3(() => {
1966
+ refreshLogFiles();
1967
+ if (selectedDate) {
1968
+ const content = readLog(selectedDate);
1969
+ setLogContent(content);
1970
+ }
1971
+ }, [refreshLogFiles, selectedDate]);
1972
+ const handleLogCreated = useCallback3(() => {
1973
+ const files = listLogFiles();
1974
+ setLogFiles(files);
1975
+ const today = getTodayDate();
1976
+ setSelectedDate(today);
1977
+ const idx = files.findIndex((f) => f.date === today);
1978
+ setHighlightedIndex(idx >= 0 ? idx : 0);
1979
+ const content = readLog(today);
1980
+ setLogContent(content);
1981
+ }, []);
1982
+ useInput12(
1983
+ (input) => {
1984
+ if (input === "5") setFocusedBox("history");
1985
+ if (input === "6") setFocusedBox("viewer");
1986
+ },
1987
+ { isActive: isFocused }
1988
+ );
1989
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", flexGrow: 1, children: [
1990
+ /* @__PURE__ */ jsx14(
1991
+ LogsHistoryBox,
1992
+ {
1993
+ logFiles,
1994
+ selectedDate,
1995
+ highlightedIndex,
1996
+ onHighlight: setHighlightedIndex,
1997
+ onSelect: handleSelectDate,
1998
+ isFocused: isFocused && focusedBox === "history"
1999
+ }
2000
+ ),
2001
+ /* @__PURE__ */ jsx14(
2002
+ LogViewerBox,
2003
+ {
2004
+ date: selectedDate,
2005
+ content: logContent,
2006
+ isFocused: isFocused && focusedBox === "viewer",
2007
+ onRefresh: handleRefresh,
2008
+ onLogCreated: handleLogCreated
2009
+ }
2010
+ )
2011
+ ] });
2012
+ }
2013
+
2014
+ // src/components/ui/KeybindingsBar.tsx
2015
+ import { Box as Box15, Text as Text14 } from "ink";
2016
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1624
2017
  var globalBindings = [
1625
2018
  { key: "1-4", label: "Focus" },
1626
2019
  { key: "j/k", label: "Navigate" },
@@ -1631,20 +2024,24 @@ var modalBindings = [
1631
2024
  ];
1632
2025
  function KeybindingsBar({ contextBindings = [], modalOpen = false }) {
1633
2026
  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 })
2027
+ return /* @__PURE__ */ jsx15(Box15, { flexShrink: 0, paddingX: 1, gap: 2, children: allBindings.map((binding) => /* @__PURE__ */ jsxs15(Box15, { gap: 1, children: [
2028
+ /* @__PURE__ */ jsx15(Text14, { bold: true, color: binding.color ?? "yellow", children: binding.key }),
2029
+ /* @__PURE__ */ jsx15(Text14, { dimColor: true, children: binding.label })
1637
2030
  ] }, binding.key)) });
1638
2031
  }
1639
2032
 
1640
2033
  // src/app.tsx
1641
- import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2034
+ import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
1642
2035
  function App() {
1643
2036
  const { exit } = useApp();
1644
- const [focusedView, setFocusedView] = useState8("github");
1645
- const [modalOpen, setModalOpen] = useState8(false);
1646
- const [contextBindings, setContextBindings] = useState8([]);
1647
- useInput10(
2037
+ const [focusedView, setFocusedView] = useState9("github");
2038
+ const [modalOpen, setModalOpen] = useState9(false);
2039
+ const [contextBindings, setContextBindings] = useState9([]);
2040
+ const [logRefreshKey, setLogRefreshKey] = useState9(0);
2041
+ const handleLogUpdated = useCallback4(() => {
2042
+ setLogRefreshKey((prev) => prev + 1);
2043
+ }, []);
2044
+ useInput13(
1648
2045
  (input, key) => {
1649
2046
  if (key.ctrl && input === "c") {
1650
2047
  exit();
@@ -1655,26 +2052,43 @@ function App() {
1655
2052
  if (input === "4") {
1656
2053
  setFocusedView("jira");
1657
2054
  }
2055
+ if (input === "5" || input === "6") {
2056
+ setFocusedView("logs");
2057
+ }
1658
2058
  },
1659
2059
  { isActive: !modalOpen }
1660
2060
  );
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 })
2061
+ return /* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "column", overflow: "hidden", children: [
2062
+ /* @__PURE__ */ jsxs16(Box16, { flexGrow: 1, flexDirection: "row", columnGap: 1, children: [
2063
+ /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
2064
+ /* @__PURE__ */ jsx16(
2065
+ GitHubView,
2066
+ {
2067
+ isFocused: focusedView === "github",
2068
+ onKeybindingsChange: focusedView === "github" ? setContextBindings : void 0,
2069
+ onLogUpdated: handleLogUpdated
2070
+ }
2071
+ ),
2072
+ /* @__PURE__ */ jsx16(
2073
+ JiraView,
2074
+ {
2075
+ isFocused: focusedView === "jira",
2076
+ onModalChange: setModalOpen,
2077
+ onKeybindingsChange: focusedView === "jira" ? setContextBindings : void 0,
2078
+ onLogUpdated: handleLogUpdated
2079
+ }
2080
+ )
2081
+ ] }),
2082
+ /* @__PURE__ */ jsx16(Box16, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: /* @__PURE__ */ jsx16(
2083
+ LogsView,
2084
+ {
2085
+ isFocused: focusedView === "logs",
2086
+ onKeybindingsChange: focusedView === "logs" ? setContextBindings : void 0,
2087
+ refreshKey: logRefreshKey
2088
+ }
2089
+ ) })
2090
+ ] }),
2091
+ /* @__PURE__ */ jsx16(KeybindingsBar, { contextBindings, modalOpen })
1678
2092
  ] });
1679
2093
  }
1680
2094
 
@@ -1682,34 +2096,34 @@ function App() {
1682
2096
  import { render as inkRender } from "ink";
1683
2097
 
1684
2098
  // 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";
2099
+ import { Box as Box17, useStdout } from "ink";
2100
+ import { useCallback as useCallback5, useEffect as useEffect7, useState as useState10 } from "react";
2101
+ import { jsx as jsx17 } from "react/jsx-runtime";
1688
2102
  function Screen({ children }) {
1689
2103
  const { stdout } = useStdout();
1690
- const getSize = useCallback3(
2104
+ const getSize = useCallback5(
1691
2105
  () => ({ height: stdout.rows, width: stdout.columns }),
1692
2106
  [stdout]
1693
2107
  );
1694
- const [size, setSize] = useState9(getSize);
1695
- useEffect6(() => {
2108
+ const [size, setSize] = useState10(getSize);
2109
+ useEffect7(() => {
1696
2110
  const onResize = () => setSize(getSize());
1697
2111
  stdout.on("resize", onResize);
1698
2112
  return () => {
1699
2113
  stdout.off("resize", onResize);
1700
2114
  };
1701
2115
  }, [stdout, getSize]);
1702
- return /* @__PURE__ */ jsx14(Box14, { height: size.height, width: size.width, children });
2116
+ return /* @__PURE__ */ jsx17(Box17, { height: size.height, width: size.width, children });
1703
2117
  }
1704
2118
 
1705
2119
  // src/lib/render.tsx
1706
- import { jsx as jsx15 } from "react/jsx-runtime";
2120
+ import { jsx as jsx18 } from "react/jsx-runtime";
1707
2121
  var ENTER_ALT_BUFFER = "\x1B[?1049h";
1708
2122
  var EXIT_ALT_BUFFER = "\x1B[?1049l";
1709
2123
  var CLEAR_SCREEN = "\x1B[2J\x1B[H";
1710
2124
  function render(node, options) {
1711
2125
  process.stdout.write(ENTER_ALT_BUFFER + CLEAR_SCREEN);
1712
- const element = /* @__PURE__ */ jsx15(Screen, { children: node });
2126
+ const element = /* @__PURE__ */ jsx18(Screen, { children: node });
1713
2127
  const instance = inkRender(element, options);
1714
2128
  setImmediate(() => instance.rerender(element));
1715
2129
  const cleanup = () => process.stdout.write(EXIT_ALT_BUFFER);
@@ -1730,7 +2144,7 @@ function render(node, options) {
1730
2144
  }
1731
2145
 
1732
2146
  // src/cli.tsx
1733
- import { jsx as jsx16 } from "react/jsx-runtime";
2147
+ import { jsx as jsx19 } from "react/jsx-runtime";
1734
2148
  meow(
1735
2149
  `
1736
2150
  Usage
@@ -1752,4 +2166,4 @@ meow(
1752
2166
  }
1753
2167
  }
1754
2168
  );
1755
- render(/* @__PURE__ */ jsx16(App, {}));
2169
+ render(/* @__PURE__ */ jsx19(App, {}));