santree 0.2.14 → 0.3.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.
- package/dist/commands/dashboard.js +45 -25
- package/dist/commands/worktree/work.d.ts +1 -1
- package/dist/commands/worktree/work.js +2 -2
- package/dist/lib/dashboard/MultilineTextArea.js +158 -17
- package/dist/lib/dashboard/Overlays.d.ts +2 -2
- package/dist/lib/dashboard/Overlays.js +6 -5
- package/dist/lib/dashboard/types.d.ts +13 -1
- package/dist/lib/dashboard/types.js +13 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useReducer, useCallback, useRef, useState } from "react";
|
|
3
3
|
import { Text, Box, useInput, useStdout, useApp } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
@@ -371,16 +371,19 @@ export default function Dashboard() {
|
|
|
371
371
|
// ── Mouse tracking pause ─────────────────────────────────────────
|
|
372
372
|
// The MultilineTextArea captures ESC for cancel. With SGR mouse tracking on,
|
|
373
373
|
// every click emits `\x1b[<btn;col;rowM` — Ink reads the leading ESC and fires
|
|
374
|
-
// key.escape, dismissing the overlay. Disable
|
|
375
|
-
// context-input
|
|
374
|
+
// key.escape, dismissing the overlay. Disable tracking while any overlay
|
|
375
|
+
// phase mounts a MultilineTextArea (context-input editing OR pr-create
|
|
376
|
+
// review); restore when that phase ends.
|
|
376
377
|
useEffect(() => {
|
|
377
|
-
|
|
378
|
+
const needsMouseOff = (state.overlay === "context-input" && state.contextInputPhase === "editing") ||
|
|
379
|
+
(state.overlay === "pr-create" && state.prCreatePhase === "review");
|
|
380
|
+
if (!needsMouseOff)
|
|
378
381
|
return;
|
|
379
382
|
process.stdout.write("\x1b[?1002l\x1b[?1006l");
|
|
380
383
|
return () => {
|
|
381
384
|
process.stdout.write("\x1b[?1002h\x1b[?1006h");
|
|
382
385
|
};
|
|
383
|
-
}, [state.overlay]);
|
|
386
|
+
}, [state.overlay, state.contextInputPhase, state.prCreatePhase]);
|
|
384
387
|
// ── Actions ───────────────────────────────────────────────────────
|
|
385
388
|
const launchWorkInTmux = useCallback((di, mode, worktreePath, contextFile) => {
|
|
386
389
|
const windowName = di.issue.identifier;
|
|
@@ -890,6 +893,9 @@ export default function Dashboard() {
|
|
|
890
893
|
}
|
|
891
894
|
// PR create overlay
|
|
892
895
|
if (state.overlay === "pr-create") {
|
|
896
|
+
// Review phase — MultilineTextArea owns the keyboard
|
|
897
|
+
if (state.prCreatePhase === "review")
|
|
898
|
+
return;
|
|
893
899
|
if (key.escape) {
|
|
894
900
|
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
895
901
|
return;
|
|
@@ -904,21 +910,17 @@ export default function Dashboard() {
|
|
|
904
910
|
return;
|
|
905
911
|
}
|
|
906
912
|
}
|
|
907
|
-
if (state.prCreatePhase === "
|
|
913
|
+
if (state.prCreatePhase === "confirm") {
|
|
908
914
|
if (input === "y" || key.return) {
|
|
909
915
|
confirmPrCreate();
|
|
910
916
|
return;
|
|
911
917
|
}
|
|
912
|
-
if (input === "
|
|
913
|
-
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
if (key.shift && key.downArrow) {
|
|
917
|
-
dispatch({ type: "SCROLL_DETAIL", offset: state.detailScrollOffset + 3 });
|
|
918
|
+
if (input === "e") {
|
|
919
|
+
dispatch({ type: "PR_CREATE_EDIT" });
|
|
918
920
|
return;
|
|
919
921
|
}
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
+
if (input === "w") {
|
|
923
|
+
openPrInWeb();
|
|
922
924
|
return;
|
|
923
925
|
}
|
|
924
926
|
}
|
|
@@ -1001,9 +1003,29 @@ export default function Dashboard() {
|
|
|
1001
1003
|
}
|
|
1002
1004
|
return;
|
|
1003
1005
|
}
|
|
1004
|
-
// Context-input overlay
|
|
1005
|
-
//
|
|
1006
|
+
// Context-input overlay.
|
|
1007
|
+
// Editing phase: MultilineTextArea owns useInput (outer is disabled
|
|
1008
|
+
// via isActive below).
|
|
1009
|
+
// Review phase: outer handles y/n/e/ESC.
|
|
1006
1010
|
if (state.overlay === "context-input") {
|
|
1011
|
+
if (state.contextInputPhase === "review") {
|
|
1012
|
+
if (input === "y" || key.return) {
|
|
1013
|
+
const mode = state.contextInputMode;
|
|
1014
|
+
const ctx = state.contextInputValue;
|
|
1015
|
+
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1016
|
+
if (mode)
|
|
1017
|
+
doWork(mode, ctx);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (input === "n" || input === "e") {
|
|
1021
|
+
dispatch({ type: "CONTEXT_INPUT_EDIT" });
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (key.escape) {
|
|
1025
|
+
dispatch({ type: "CONTEXT_INPUT_DONE" });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1007
1029
|
return;
|
|
1008
1030
|
}
|
|
1009
1031
|
// Confirm delete overlay
|
|
@@ -1475,7 +1497,8 @@ export default function Dashboard() {
|
|
|
1475
1497
|
return;
|
|
1476
1498
|
}
|
|
1477
1499
|
}, {
|
|
1478
|
-
isActive: state.overlay !== "context-input" &&
|
|
1500
|
+
isActive: (state.overlay !== "context-input" || state.contextInputPhase === "review") &&
|
|
1501
|
+
(state.overlay !== "pr-create" || state.prCreatePhase !== "review") &&
|
|
1479
1502
|
(state.overlay !== "commit" || state.commitPhase !== "awaiting-message"),
|
|
1480
1503
|
});
|
|
1481
1504
|
// ── Render ─────────────────────────────────────────────────────────
|
|
@@ -1487,13 +1510,10 @@ export default function Dashboard() {
|
|
|
1487
1510
|
}
|
|
1488
1511
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
1489
1512
|
const selectedReview = state.flatReviews[state.reviewSelectedIndex] ?? null;
|
|
1490
|
-
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" ", "claude ", CLAUDE_VERSION] })) : null, _jsx(Text, { dimColor: true, children: state.refreshing ? " refreshing..." : "" }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), _jsxs(Box, { children: [_jsxs(Text, { bold: state.activeTab === "issues", color: state.activeTab === "issues" ? "cyan" : undefined, dimColor: state.activeTab !== "issues", children: [state.activeTab === "issues" ? "\u25b8 " : " ", "1 Issues (", state.flatIssues.length, ")"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: state.activeTab === "reviews", color: state.activeTab === "reviews" ? "cyan" : undefined, dimColor: state.activeTab !== "reviews", children: [state.activeTab === "reviews" ? "\u25b8 " : " ", "2 Reviews (", state.flatReviews.length, ")"] })] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
if (mode)
|
|
1495
|
-
doWork(mode, ctx);
|
|
1496
|
-
}, onCancel: () => dispatch({ type: "CONTEXT_INPUT_DONE" }), width: Math.min(columns - 8, 100), height: 10, placeholder: "Type or paste extra context\u2026" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " ", "launch", " ", _jsx(Text, { color: "cyan", bold: true, children: "Enter" }), " ", "newline", " ", _jsx(Text, { color: "cyan", bold: true, children: "ESC" }), " ", "cancel"] })] }) })) : state.overlay === "base-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select base branch:" }), _jsx(Text, { children: " " }), state.baseSelectOptions.map((branch, idx) => {
|
|
1513
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" ", "v", version] }), CLAUDE_VERSION ? (_jsxs(Text, { dimColor: true, children: [" ", "claude ", CLAUDE_VERSION] })) : null, _jsx(Text, { dimColor: true, children: state.refreshing ? " refreshing..." : "" }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), _jsxs(Box, { children: [_jsxs(Text, { bold: state.activeTab === "issues", color: state.activeTab === "issues" ? "cyan" : undefined, dimColor: state.activeTab !== "issues", children: [state.activeTab === "issues" ? "\u25b8 " : " ", "1 Issues (", state.flatIssues.length, ")"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: state.activeTab === "reviews", color: state.activeTab === "reviews" ? "cyan" : undefined, dimColor: state.activeTab !== "reviews", children: [state.activeTab === "reviews" ? "\u25b8 " : " ", "2 Reviews (", state.flatReviews.length, ")"] })] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "context-input" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", paddingX: 2, width: Math.min(columns - 8, 100), children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Extra context for ", state.contextInputMode] }), _jsx(Text, { dimColor: true, children: "Optional \u2014 appended to the prompt before launching Claude" }), _jsx(Text, { children: " " }), state.contextInputPhase === "editing" ? (_jsxs(_Fragment, { children: [_jsx(MultilineTextArea, { value: state.contextInputValue, onChange: (v) => dispatch({ type: "CONTEXT_INPUT_CHANGE", value: v }), onSubmit: () => dispatch({ type: "CONTEXT_INPUT_REVIEW" }), onCancel: () => dispatch({ type: "CONTEXT_INPUT_DONE" }), width: Math.min(columns - 8, 100), height: 10, placeholder: "Type or paste extra context\u2026" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Enter" }), " newline ", _jsx(Text, { color: "cyan", bold: true, children: "\u2191\u2193\u2190\u2192" }), " move ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+V" }), " paste image ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " continue ", _jsx(Text, { color: "cyan", bold: true, children: "ESC" }), " cancel"] })] })) : (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, minHeight: 6, children: [(state.contextInputValue || "(no extra context)")
|
|
1514
|
+
.split("\n")
|
|
1515
|
+
.slice(0, 12)
|
|
1516
|
+
.map((line, i) => (_jsx(Text, { children: line || " " }, i))), state.contextInputValue.split("\n").length > 12 && (_jsxs(Text, { dimColor: true, children: ["\u2026+", state.contextInputValue.split("\n").length - 12, " more lines"] }))] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Anything else to add?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " / ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), " launch ", _jsx(Text, { color: "yellow", bold: true, children: "n" }), " / ", _jsx(Text, { color: "yellow", bold: true, children: "e" }), " keep editing ", _jsx(Text, { color: "red", bold: true, children: "ESC" }), " cancel"] })] }))] }) })) : state.overlay === "base-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select base branch:" }), _jsx(Text, { children: " " }), state.baseSelectOptions.map((branch, idx) => {
|
|
1497
1517
|
const selected = idx === state.baseSelectIndex;
|
|
1498
1518
|
const defaultBranch = getDefaultBranch();
|
|
1499
1519
|
const label = branch === defaultBranch ? `${branch} (default)` : branch;
|
|
@@ -1501,5 +1521,5 @@ export default function Dashboard() {
|
|
|
1501
1521
|
}), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
|
|
1502
1522
|
.split("\n")
|
|
1503
1523
|
.slice(-(contentHeight - 1))
|
|
1504
|
-
.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle,
|
|
1524
|
+
.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
1505
1525
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const description = "Launch Claude to work on current ticket";
|
|
3
3
|
export declare const options: z.ZodObject<{
|
|
4
4
|
plan: z.ZodOptional<z.ZodBoolean>;
|
|
5
|
-
|
|
5
|
+
contextFile: z.ZodOptional<z.ZodString>;
|
|
6
6
|
}, z.core.$strip>;
|
|
7
7
|
type Props = {
|
|
8
8
|
options: z.infer<typeof options>;
|
|
@@ -10,7 +10,7 @@ import { getSessionId, setSessionId } from "../../lib/git.js";
|
|
|
10
10
|
export const description = "Launch Claude to work on current ticket";
|
|
11
11
|
export const options = z.object({
|
|
12
12
|
plan: z.boolean().optional().describe("Only create implementation plan"),
|
|
13
|
-
|
|
13
|
+
contextFile: z
|
|
14
14
|
.string()
|
|
15
15
|
.optional()
|
|
16
16
|
.describe("Path to a file whose contents are appended to the prompt as extra context"),
|
|
@@ -28,7 +28,7 @@ export default function Work({ options }) {
|
|
|
28
28
|
const [error, setError] = useState(null);
|
|
29
29
|
const [mode] = useState(options.plan ? "plan" : "implement");
|
|
30
30
|
const [aiContext, setAiContext] = useState(null);
|
|
31
|
-
const contextFilePath = options
|
|
31
|
+
const contextFilePath = options.contextFile;
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
async function init() {
|
|
34
34
|
// Small delay to allow spinner to render
|
|
@@ -1,48 +1,189 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
2
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { openSync, readSync, closeSync, statSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
9
|
+
// macOS clipboard → PNG. Returns the written file path on success, or null if
|
|
10
|
+
// the clipboard holds no image, the platform isn't macOS, or the write produced
|
|
11
|
+
// a file that isn't actually a PNG. The coercion to «class PNGf» errors when
|
|
12
|
+
// the clipboard holds only text — verified against real clipboards.
|
|
13
|
+
function pasteClipboardImageToTmp() {
|
|
14
|
+
if (process.platform !== "darwin")
|
|
15
|
+
return null;
|
|
16
|
+
const filePath = join(tmpdir(), `santree-paste-${Date.now()}.png`);
|
|
17
|
+
const script = `try
|
|
18
|
+
set pngData to the clipboard as «class PNGf»
|
|
19
|
+
set theFile to POSIX file "${filePath}"
|
|
20
|
+
set fileRef to open for access theFile with write permission
|
|
21
|
+
set eof fileRef to 0
|
|
22
|
+
write pngData to fileRef
|
|
23
|
+
close access fileRef
|
|
24
|
+
return "ok"
|
|
25
|
+
on error
|
|
26
|
+
return "no-image"
|
|
27
|
+
end try`;
|
|
28
|
+
try {
|
|
29
|
+
const result = spawnSync("osascript", ["-e", script], {
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
timeout: 3000,
|
|
32
|
+
});
|
|
33
|
+
if (result.status !== 0 || result.stdout.trim() !== "ok")
|
|
34
|
+
return null;
|
|
35
|
+
// Defense in depth: verify the file is non-empty and starts with the PNG
|
|
36
|
+
// magic header. Guards against an osascript quirk writing a stub.
|
|
37
|
+
if (statSync(filePath).size === 0) {
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(filePath);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const fd = openSync(filePath, "r");
|
|
45
|
+
const header = Buffer.alloc(8);
|
|
46
|
+
readSync(fd, header, 0, 8, 0);
|
|
47
|
+
closeSync(fd);
|
|
48
|
+
if (!header.equals(PNG_MAGIC)) {
|
|
49
|
+
try {
|
|
50
|
+
unlinkSync(filePath);
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return filePath;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// osascript unavailable or fs error — silent no-op
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function offsetToRowCol(value, offset) {
|
|
63
|
+
const lines = value.split("\n");
|
|
64
|
+
let idx = 0;
|
|
65
|
+
for (let r = 0; r < lines.length; r++) {
|
|
66
|
+
const len = lines[r].length;
|
|
67
|
+
if (offset <= idx + len) {
|
|
68
|
+
return [r, offset - idx];
|
|
69
|
+
}
|
|
70
|
+
idx += len + 1;
|
|
71
|
+
}
|
|
72
|
+
const last = lines.length - 1;
|
|
73
|
+
return [last, lines[last].length];
|
|
74
|
+
}
|
|
75
|
+
function rowColToOffset(value, row, col) {
|
|
76
|
+
const lines = value.split("\n");
|
|
77
|
+
const clampedRow = Math.max(0, Math.min(row, lines.length - 1));
|
|
78
|
+
let idx = 0;
|
|
79
|
+
for (let r = 0; r < clampedRow; r++) {
|
|
80
|
+
idx += lines[r].length + 1;
|
|
81
|
+
}
|
|
82
|
+
const clampedCol = Math.max(0, Math.min(col, lines[clampedRow].length));
|
|
83
|
+
return idx + clampedCol;
|
|
84
|
+
}
|
|
3
85
|
export function MultilineTextArea({ value, onChange, onSubmit, onCancel, placeholder, width, height = 6, focus = true, }) {
|
|
86
|
+
const [cursor, setCursor] = useState(value.length);
|
|
87
|
+
// Keep cursor within bounds if value shrinks externally
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (cursor > value.length)
|
|
90
|
+
setCursor(value.length);
|
|
91
|
+
}, [value, cursor]);
|
|
92
|
+
const insertAt = (pos, text) => {
|
|
93
|
+
onChange(value.slice(0, pos) + text + value.slice(pos));
|
|
94
|
+
setCursor(pos + text.length);
|
|
95
|
+
};
|
|
96
|
+
const deleteBefore = (pos) => {
|
|
97
|
+
if (pos === 0)
|
|
98
|
+
return;
|
|
99
|
+
onChange(value.slice(0, pos - 1) + value.slice(pos));
|
|
100
|
+
setCursor(pos - 1);
|
|
101
|
+
};
|
|
4
102
|
useInput((input, key) => {
|
|
5
103
|
// Ctrl+D submits
|
|
6
104
|
if (key.ctrl && input === "d") {
|
|
7
105
|
onSubmit();
|
|
8
106
|
return;
|
|
9
107
|
}
|
|
10
|
-
//
|
|
11
|
-
//
|
|
108
|
+
// Ctrl+V — try to paste clipboard image as a temp file reference.
|
|
109
|
+
// Regular Cmd+V text paste is handled by the terminal and arrives as
|
|
110
|
+
// normal input below.
|
|
111
|
+
if (key.ctrl && input === "v") {
|
|
112
|
+
const imagePath = pasteClipboardImageToTmp();
|
|
113
|
+
if (imagePath) {
|
|
114
|
+
insertAt(cursor, ``);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// ESC cancels (parent disables SGR mouse tracking while mounted
|
|
119
|
+
// so clicks don't masquerade as ESC)
|
|
12
120
|
if (key.escape) {
|
|
13
121
|
onCancel();
|
|
14
122
|
return;
|
|
15
123
|
}
|
|
16
124
|
if (key.backspace || key.delete) {
|
|
17
|
-
|
|
125
|
+
deleteBefore(cursor);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Arrow navigation — column is remembered via col-from-current-pos
|
|
129
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
130
|
+
const lines = value.split("\n");
|
|
131
|
+
const [row, col] = offsetToRowCol(value, cursor);
|
|
132
|
+
if (key.upArrow) {
|
|
133
|
+
if (row === 0)
|
|
134
|
+
setCursor(0);
|
|
135
|
+
else
|
|
136
|
+
setCursor(rowColToOffset(value, row - 1, col));
|
|
137
|
+
}
|
|
138
|
+
else if (key.downArrow) {
|
|
139
|
+
if (row === lines.length - 1)
|
|
140
|
+
setCursor(value.length);
|
|
141
|
+
else
|
|
142
|
+
setCursor(rowColToOffset(value, row + 1, col));
|
|
143
|
+
}
|
|
144
|
+
else if (key.leftArrow) {
|
|
145
|
+
setCursor(Math.max(0, cursor - 1));
|
|
146
|
+
}
|
|
147
|
+
else if (key.rightArrow) {
|
|
148
|
+
setCursor(Math.min(value.length, cursor + 1));
|
|
149
|
+
}
|
|
18
150
|
return;
|
|
19
151
|
}
|
|
20
|
-
|
|
21
|
-
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow || key.tab)
|
|
152
|
+
if (key.tab)
|
|
22
153
|
return;
|
|
23
|
-
// Enter inserts a newline. MUST run before
|
|
24
|
-
// Option+Enter
|
|
25
|
-
//
|
|
26
|
-
// \r — normalize and append the whole thing instead of dropping it.
|
|
154
|
+
// Enter inserts a newline at cursor. MUST run before meta/ctrl swallow
|
|
155
|
+
// so Option+Enter / Ctrl+Enter also insert. When a paste carries content
|
|
156
|
+
// alongside \r, append the whole normalized chunk.
|
|
27
157
|
if (key.return) {
|
|
28
158
|
const chunk = input ? input.replace(/\r\n?/g, "\n") : "\n";
|
|
29
|
-
|
|
159
|
+
insertAt(cursor, chunk);
|
|
30
160
|
return;
|
|
31
161
|
}
|
|
32
|
-
// Swallow remaining modifier combos
|
|
162
|
+
// Swallow remaining modifier combos
|
|
33
163
|
if (key.ctrl || key.meta)
|
|
34
164
|
return;
|
|
35
165
|
if (!input)
|
|
36
166
|
return;
|
|
37
|
-
// Pasted content may embed \r or \r\n — normalize to \n.
|
|
38
167
|
const normalized = input.replace(/\r\n?/g, "\n");
|
|
39
|
-
|
|
168
|
+
insertAt(cursor, normalized);
|
|
40
169
|
}, { isActive: focus });
|
|
170
|
+
const [cursorRow, cursorCol] = offsetToRowCol(value, cursor);
|
|
41
171
|
const lines = value.length === 0 ? [""] : value.split("\n");
|
|
42
|
-
|
|
172
|
+
// Scroll viewport so the cursor row is always visible
|
|
173
|
+
let scrollStart = 0;
|
|
174
|
+
if (cursorRow >= height)
|
|
175
|
+
scrollStart = cursorRow - height + 1;
|
|
176
|
+
const visibleLines = lines.slice(scrollStart, scrollStart + height);
|
|
43
177
|
const isEmpty = value.length === 0;
|
|
44
|
-
return (_jsx(Box, { flexDirection: "column", width: width, borderStyle: "round", borderColor: "cyan", paddingX: 1, minHeight: height + 2, children: isEmpty && placeholder ? (_jsxs(Box, { minHeight: 1, children: [_jsx(Text, {
|
|
45
|
-
const
|
|
46
|
-
|
|
178
|
+
return (_jsx(Box, { flexDirection: "column", width: width, borderStyle: "round", borderColor: "cyan", paddingX: 1, minHeight: height + 2, children: isEmpty && placeholder ? (_jsxs(Box, { minHeight: 1, children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: placeholder })] })) : (visibleLines.map((line, i) => {
|
|
179
|
+
const absoluteRow = scrollStart + i;
|
|
180
|
+
const isCursorRow = focus && absoluteRow === cursorRow;
|
|
181
|
+
if (!isCursorRow) {
|
|
182
|
+
return (_jsx(Box, { minHeight: 1, children: _jsx(Text, { children: line }) }, i));
|
|
183
|
+
}
|
|
184
|
+
const before = line.slice(0, cursorCol);
|
|
185
|
+
const atCursor = cursorCol < line.length ? line[cursorCol] : " ";
|
|
186
|
+
const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
|
|
187
|
+
return (_jsxs(Box, { minHeight: 1, children: [_jsx(Text, { children: before }), _jsx(Text, { inverse: true, children: atCursor }), _jsx(Text, { children: after })] }, i));
|
|
47
188
|
})) }));
|
|
48
189
|
}
|
|
@@ -22,7 +22,7 @@ interface PrCreateOverlayProps {
|
|
|
22
22
|
url: string | null;
|
|
23
23
|
body: string | null;
|
|
24
24
|
title: string | null;
|
|
25
|
-
|
|
25
|
+
dispatch: React.Dispatch<DashboardAction>;
|
|
26
26
|
}
|
|
27
|
-
export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title,
|
|
27
|
+
export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
28
28
|
export {};
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
|
+
import { MultilineTextArea } from "./MultilineTextArea.js";
|
|
5
6
|
export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phase, message, error, dispatch, onSubmit, }) {
|
|
6
7
|
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Commit & Push" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), gitStatus ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Changes:" }), gitStatus
|
|
7
8
|
.split("\n")
|
|
@@ -20,9 +21,9 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
|
|
|
20
21
|
return (_jsxs(Text, { color: color, children: [" ", line] }, i));
|
|
21
22
|
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "awaiting-message" && (_jsxs(Box, { children: [_jsx(Text, { children: "Message: " }), _jsx(TextInput, { value: message, onChange: (v) => dispatch({ type: "COMMIT_MESSAGE", message: v }), onSubmit: onSubmit })] })), phase === "committing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Committing..."] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing..."] })), phase === "done" && (_jsx(Text, { color: "green", bold: true, children: "Committed and pushed!" })), phase === "error" && _jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }));
|
|
22
23
|
}
|
|
23
|
-
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title,
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }) {
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit PR description" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: body ?? "", onChange: (v) => dispatch({ type: "PR_CREATE_BODY_CHANGE", body: v }), onSubmit: () => dispatch({ type: "PR_CREATE_CONFIRM" }), onCancel: () => dispatch({ type: "PR_CREATE_CANCEL" }), width: width, height: Math.max(6, height - 10), placeholder: "(empty PR body)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Enter" }), " newline ", _jsx(Text, { color: "cyan", bold: true, children: "\u2191\u2193\u2190\u2192" }), " move ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+V" }), " paste image ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " continue ", _jsx(Text, { color: "cyan", bold: true, children: "ESC" }), " cancel"] })] })), phase === "confirm" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [(body ?? "")
|
|
26
|
+
.split("\n")
|
|
27
|
+
.slice(0, Math.max(4, height - 12))
|
|
28
|
+
.map((line, i) => (_jsx(Text, { wrap: "truncate", children: line || " " }, i))), (body ?? "").split("\n").length > Math.max(4, height - 12) && (_jsxs(Text, { dimColor: true, children: ["\u2026+", (body ?? "").split("\n").length - Math.max(4, height - 12), " more lines"] }))] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " / ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), " create ", _jsx(Text, { color: "yellow", bold: true, children: "e" }), " keep editing ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " open in browser ", _jsx(Text, { color: "red", bold: true, children: "ESC" }), " cancel"] })] })), phase === "creating" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Creating PR..."] })), phase === "done" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "PR created!" }), url ? _jsx(Text, { dimColor: true, children: url }) : null] })), phase === "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "open in browser ESC cancel"] })] })), phase !== "review" && phase !== "confirm" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
28
29
|
}
|
|
@@ -60,7 +60,7 @@ export interface EnrichedReviewPR {
|
|
|
60
60
|
export type DashboardTab = "issues" | "reviews";
|
|
61
61
|
export type ActionOverlay = "mode-select" | "context-input" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
|
|
62
62
|
export type CommitPhase = "idle" | "confirm-stage" | "awaiting-message" | "committing" | "pushing" | "done" | "error";
|
|
63
|
-
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "filling" | "review" | "creating" | "done" | "error";
|
|
63
|
+
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "filling" | "review" | "confirm" | "creating" | "done" | "error";
|
|
64
64
|
export interface DashboardState {
|
|
65
65
|
activeTab: DashboardTab;
|
|
66
66
|
groups: ProjectGroup[];
|
|
@@ -102,6 +102,7 @@ export interface DashboardState {
|
|
|
102
102
|
baseSelectChosen: string | null;
|
|
103
103
|
contextInputValue: string;
|
|
104
104
|
contextInputMode: "plan" | "implement" | null;
|
|
105
|
+
contextInputPhase: "editing" | "review";
|
|
105
106
|
}
|
|
106
107
|
export type DashboardAction = {
|
|
107
108
|
type: "SET_DATA";
|
|
@@ -181,6 +182,13 @@ export type DashboardAction = {
|
|
|
181
182
|
type: "PR_CREATE_REVIEW";
|
|
182
183
|
body: string;
|
|
183
184
|
title: string;
|
|
185
|
+
} | {
|
|
186
|
+
type: "PR_CREATE_BODY_CHANGE";
|
|
187
|
+
body: string;
|
|
188
|
+
} | {
|
|
189
|
+
type: "PR_CREATE_CONFIRM";
|
|
190
|
+
} | {
|
|
191
|
+
type: "PR_CREATE_EDIT";
|
|
184
192
|
} | {
|
|
185
193
|
type: "PR_CREATE_DONE";
|
|
186
194
|
url: string;
|
|
@@ -223,6 +231,10 @@ export type DashboardAction = {
|
|
|
223
231
|
} | {
|
|
224
232
|
type: "CONTEXT_INPUT_CHANGE";
|
|
225
233
|
value: string;
|
|
234
|
+
} | {
|
|
235
|
+
type: "CONTEXT_INPUT_REVIEW";
|
|
236
|
+
} | {
|
|
237
|
+
type: "CONTEXT_INPUT_EDIT";
|
|
226
238
|
} | {
|
|
227
239
|
type: "CONTEXT_INPUT_DONE";
|
|
228
240
|
};
|
|
@@ -40,6 +40,7 @@ export const initialState = {
|
|
|
40
40
|
baseSelectChosen: null,
|
|
41
41
|
contextInputValue: "",
|
|
42
42
|
contextInputMode: null,
|
|
43
|
+
contextInputPhase: "editing",
|
|
43
44
|
};
|
|
44
45
|
export function reducer(state, action) {
|
|
45
46
|
switch (action.type) {
|
|
@@ -165,6 +166,12 @@ export function reducer(state, action) {
|
|
|
165
166
|
prCreateTitle: action.title,
|
|
166
167
|
detailScrollOffset: 0,
|
|
167
168
|
};
|
|
169
|
+
case "PR_CREATE_BODY_CHANGE":
|
|
170
|
+
return { ...state, prCreateBody: action.body };
|
|
171
|
+
case "PR_CREATE_CONFIRM":
|
|
172
|
+
return { ...state, prCreatePhase: "confirm" };
|
|
173
|
+
case "PR_CREATE_EDIT":
|
|
174
|
+
return { ...state, prCreatePhase: "review" };
|
|
168
175
|
case "PR_CREATE_DONE":
|
|
169
176
|
return {
|
|
170
177
|
...state,
|
|
@@ -250,15 +257,21 @@ export function reducer(state, action) {
|
|
|
250
257
|
overlay: "context-input",
|
|
251
258
|
contextInputMode: action.mode,
|
|
252
259
|
contextInputValue: "",
|
|
260
|
+
contextInputPhase: "editing",
|
|
253
261
|
};
|
|
254
262
|
case "CONTEXT_INPUT_CHANGE":
|
|
255
263
|
return { ...state, contextInputValue: action.value };
|
|
264
|
+
case "CONTEXT_INPUT_REVIEW":
|
|
265
|
+
return { ...state, contextInputPhase: "review" };
|
|
266
|
+
case "CONTEXT_INPUT_EDIT":
|
|
267
|
+
return { ...state, contextInputPhase: "editing" };
|
|
256
268
|
case "CONTEXT_INPUT_DONE":
|
|
257
269
|
return {
|
|
258
270
|
...state,
|
|
259
271
|
overlay: null,
|
|
260
272
|
contextInputMode: null,
|
|
261
273
|
contextInputValue: "",
|
|
274
|
+
contextInputPhase: "editing",
|
|
262
275
|
};
|
|
263
276
|
default:
|
|
264
277
|
return state;
|