Skip to content

Commit fb87dea

Browse files
committed
Merge branch 'main' into feature/changelog
2 parents 439f1eb + 1050e65 commit fb87dea

11 files changed

Lines changed: 91 additions & 44 deletions

File tree

packages/cli/src/cli-args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import type { Argv } from "yargs";
77
import Yargs from "yargs";
88
import { getCliVersion } from "./utils/version";
9-
import { writeStderrLine } from "./utils/stdioHelpers";
9+
import { writeStderrLine } from "./utils/stdio-helpers";
1010
import { hideBin } from "yargs/helpers";
1111

1212
// UUID v4 regex pattern for validation

packages/cli/src/cli.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core";
77
import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check";
88
import { AppContainer } from "./ui";
99
import { parseArguments } from "./cli-args";
10-
import { writeStderrLine, writeStdoutLine } from "./utils/stdioHelpers";
10+
import { writeStderrLine, writeStdoutLine } from "./utils/stdio-helpers";
1111
import { getPackageJson } from "./utils/package";
1212
import { CLI_VERSION } from "./generated/git-commit";
1313

packages/cli/src/tests/exit-summary.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from "node:test";
22
import assert from "node:assert/strict";
3-
import { buildExitSummaryText } from "../ui";
3+
import { buildExitSummaryText, buildResumeHintText } from "../ui";
44
import type { ModelUsage, SessionEntry } from "@vegamo/deepcode-core";
55

66
const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, "");
@@ -90,7 +90,7 @@ test("buildExitSummaryText does not derive usage rows from legacy aggregate usag
9090
assert.doesNotMatch(summary, /11,966/);
9191
});
9292

93-
test("buildExitSummaryText shows resume hint when sessionId is provided", () => {
93+
test("buildExitSummaryText does not show resume hint when sessionId is provided", () => {
9494
const sessionId = "0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6";
9595
const summary = stripAnsi(
9696
buildExitSummaryText({
@@ -100,8 +100,8 @@ test("buildExitSummaryText shows resume hint when sessionId is provided", () =>
100100
);
101101

102102
assert.match(summary, /Goodbye!/);
103-
assert.match(summary, /deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6/);
104-
assert.match(summary, /To continue this session/);
103+
assert.doesNotMatch(summary, /deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6/);
104+
assert.doesNotMatch(summary, /To continue this session/);
105105
});
106106

107107
test("buildExitSummaryText does not show resume hint when sessionId is omitted", () => {
@@ -116,7 +116,7 @@ test("buildExitSummaryText does not show resume hint when sessionId is omitted",
116116
assert.doesNotMatch(summary, /To continue this session/);
117117
});
118118

119-
test("buildExitSummaryText shows resume hint with null session", () => {
119+
test("buildExitSummaryText does not show resume hint with null session", () => {
120120
const summary = stripAnsi(
121121
buildExitSummaryText({
122122
session: null,
@@ -125,7 +125,18 @@ test("buildExitSummaryText shows resume hint with null session", () => {
125125
);
126126

127127
assert.match(summary, /Goodbye!/);
128-
assert.match(summary, /deepcode --resume test-session-id/);
128+
assert.doesNotMatch(summary, /deepcode --resume test-session-id/);
129+
assert.doesNotMatch(summary, /To continue this session/);
130+
});
131+
132+
test("buildResumeHintText shows resume command when sessionId is provided", () => {
133+
const hint = stripAnsi(buildResumeHintText("0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6") ?? "");
134+
135+
assert.equal(hint, "To continue this session, run deepcode --resume 0a5cb7a5-c39d-4c39-a11b-05f8b22b8df6");
136+
});
137+
138+
test("buildResumeHintText returns null when sessionId is omitted", () => {
139+
assert.equal(buildResumeHintText(), null);
129140
});
130141

131142
function buildSession(usage: ModelUsage | null, usagePerModel: Record<string, ModelUsage> | null = null): SessionEntry {

packages/cli/src/tests/message-view.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,19 @@ test("renderMessageToStdout shows (no content) for empty user messages", () => {
127127
});
128128

129129
test("MessageView echoes submitted user prompts with live prompt wrapping width", () => {
130-
assert.equal(getPromptEchoContentWidth(8), 6);
130+
assert.equal(getPromptEchoContentWidth(8), 5);
131131

132132
const msg = makeSessionMessage({ role: "user", content: "abcdefg" });
133133
const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 });
134134

135-
assert.equal(stripAnsi(output), "> abcdef\n g\n");
135+
const text = stripAnsi(output);
136+
assert.equal(text, " > abcde\n fg\n");
137+
assert.ok(
138+
text
139+
.trimEnd()
140+
.split("\n")
141+
.every((line) => line.length <= 8)
142+
);
136143
});
137144

138145
test("MessageView echoes model changes with submitted prompt wrapping", () => {
@@ -143,7 +150,14 @@ test("MessageView echoes model changes with submitted prompt wrapping", () => {
143150
});
144151
const output = renderToString(React.createElement(MessageView, { message: msg, width: 8 }), { columns: 8 });
145152

146-
assert.equal(stripAnsi(output), "> abcdef\n gh\n");
153+
const text = stripAnsi(output);
154+
assert.equal(text, " > abcde\n fgh\n");
155+
assert.ok(
156+
text
157+
.trimEnd()
158+
.split("\n")
159+
.every((line) => line.length <= 8)
160+
);
147161
});
148162

149163
test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => {

packages/cli/src/ui/components/MessageView/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DiffPreviewLine, MessageViewProps } from "./types";
1313
import { RawMode, useRawModeContext } from "../../contexts";
1414

1515
const PROMPT_ECHO_PREFIX_WIDTH = 2;
16+
const PROMPT_ECHO_MARGIN_LEFT = 1;
1617

1718
export function MessageView({ message, collapsed, width = 80 }: MessageViewProps): React.ReactElement | null {
1819
const { mode } = useRawModeContext();
@@ -131,7 +132,7 @@ export function MessageView({ message, collapsed, width = 80 }: MessageViewProps
131132
}
132133

133134
export function getPromptEchoContentWidth(width: number): number {
134-
return Math.max(1, width - PROMPT_ECHO_PREFIX_WIDTH);
135+
return Math.max(1, width - PROMPT_ECHO_MARGIN_LEFT - PROMPT_ECHO_PREFIX_WIDTH);
135136
}
136137

137138
function PromptEchoLine({
@@ -144,8 +145,9 @@ function PromptEchoLine({
144145
attachmentCount?: number;
145146
}): React.ReactElement {
146147
const contentWidth = getPromptEchoContentWidth(width);
148+
const containerWidth = Math.max(1, width - PROMPT_ECHO_MARGIN_LEFT);
147149
return (
148-
<Box marginBottom={1} marginY={0} width={Math.max(1, width)} flexDirection="row">
150+
<Box marginBottom={1} marginLeft={PROMPT_ECHO_MARGIN_LEFT} marginY={0} width={containerWidth} flexDirection="row">
149151
<Box width={PROMPT_ECHO_PREFIX_WIDTH}>
150152
<Text color="#229ac3">{"> "}</Text>
151153
</Box>

packages/cli/src/ui/exit-summary.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function extractUsageFields(usage: ModelUsage | null): UsageFields {
6868
}
6969

7070
export function buildExitSummaryText(input: ExitSummaryInput): string {
71-
const { session, sessionId } = input;
71+
const { session } = input;
7272

7373
const innerWidth = 98;
7474
const contentWidth = innerWidth - 4; // "│ " prefix + " │" suffix → 4 chars padding
@@ -135,13 +135,6 @@ export function buildExitSummaryText(input: ExitSummaryInput): string {
135135

136136
rows.push("");
137137

138-
if (sessionId) {
139-
const resumeHint =
140-
chalk.dim(`To continue this session, run `) + chalk.hex("#229ac3")(`deepcode --resume ${sessionId}`);
141-
rows.push(resumeHint);
142-
rows.push("");
143-
}
144-
145138
const border = borderColor("─".repeat(innerWidth));
146139
const top = `${borderColor("╭")}${border}${borderColor("╮")}`;
147140
const bottom = `${borderColor("╰")}${border}${borderColor("╯")}`;
@@ -150,3 +143,10 @@ export function buildExitSummaryText(input: ExitSummaryInput): string {
150143

151144
return [top, body, bottom].join("\n");
152145
}
146+
147+
export function buildResumeHintText(sessionId?: string): string | null {
148+
if (!sessionId) {
149+
return null;
150+
}
151+
return chalk.dim(`To continue this session, run `) + chalk.hex("#229ac3")(`deepcode --resume ${sessionId}`);
152+
}

packages/cli/src/ui/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ export {
9090
type FileMentionToken,
9191
} from "./core/file-mentions";
9292
export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinking-state";
93-
export { buildExitSummaryText } from "./exit-summary";
93+
export { buildExitSummaryText, buildResumeHintText } from "./exit-summary";

packages/cli/src/ui/views/App.tsx

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
formatAskUserQuestionAnswers,
2121
} from "../core/ask-user-question";
2222
import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt";
23-
import { buildExitSummaryText } from "../exit-summary";
23+
import { buildExitSummaryText, buildResumeHintText } from "../exit-summary";
2424
import { RawMode, useRawModeContext } from "../contexts";
2525
import { renderMessageToStdout } from "../components/MessageView/utils";
2626
import {
@@ -290,22 +290,40 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
290290
}, [sessionManager]);
291291

292292
writeRef.current = write;
293-
const handlePrompt = useCallback(
294-
async (submission: PromptSubmission) => {
295-
if (submission.command === "exit") {
296-
setIsExiting(true);
297-
setTimeout(() => {
298-
const activeSessionId = sessionManager.getActiveSessionId();
299-
const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null;
300-
const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined });
301-
process.stdout.write("\n");
293+
const handleExit = useCallback(
294+
({ showCommand, showSummary }: { showCommand: boolean; showSummary: boolean }) => {
295+
setIsExiting(true);
296+
setTimeout(() => {
297+
const activeSessionId = sessionManager.getActiveSessionId();
298+
const session = activeSessionId ? sessionManager.getSession(activeSessionId) : null;
299+
const resumeHint = buildResumeHintText(activeSessionId ?? undefined);
300+
301+
process.stdout.write("\n");
302+
if (showCommand) {
302303
process.stdout.write(chalk.rgb(34, 154, 195)("> /exit "));
303304
process.stdout.write("\n\n");
305+
}
306+
if (showSummary) {
307+
const summary = buildExitSummaryText({ session, sessionId: activeSessionId ?? undefined });
304308
process.stdout.write(summary);
305309
process.stdout.write("\n\n");
306-
sessionManager.dispose();
307-
exit();
308-
}, 0);
310+
}
311+
if (resumeHint) {
312+
process.stdout.write(resumeHint);
313+
process.stdout.write("\n");
314+
}
315+
316+
sessionManager.dispose();
317+
exit();
318+
}, 0);
319+
},
320+
[exit, sessionManager]
321+
);
322+
323+
const handlePrompt = useCallback(
324+
async (submission: PromptSubmission) => {
325+
if (submission.command === "exit") {
326+
handleExit({ showCommand: true, showSummary: true });
309327
return;
310328
}
311329
if (submission.command === "new") {
@@ -400,7 +418,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
400418
[
401419
sessionManager,
402420
pendingPermissionReply,
403-
exit,
421+
handleExit,
404422
onRestart,
405423
refreshSkills,
406424
refreshSessionsList,
@@ -477,6 +495,10 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
477495
[handlePrompt]
478496
);
479497

498+
const handleExitShortcut = useCallback(() => {
499+
handleExit({ showCommand: false, showSummary: false });
500+
}, [handleExit]);
501+
480502
const reloadActiveSessionView = useCallback(
481503
(sessionId: string): void => {
482504
resetStaticView(loadVisibleMessages(sessionManager, sessionId), { clearScreen: true });
@@ -959,6 +981,7 @@ function App({ projectRoot, initialPrompt, resumeSessionId, onRestart }: AppProp
959981
onRawModeChange={handleRawModeChange}
960982
onInterrupt={handleInterrupt}
961983
onToggleProcessStdout={handleToggleProcessStdout}
984+
onExitShortcut={handleExitShortcut}
962985
placeholder="Type your message..."
963986
statusLineSegments={statusLineSegments}
964987
statusLineSeparator={resolvedSettings.statusline.separator}

packages/cli/src/ui/views/PromptInput.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
2-
import { Box, Text, useApp, useStdout } from "ink";
2+
import { Box, Text, useStdout } from "ink";
33
import type { DOMElement } from "ink";
44
import chalk from "chalk";
55
import { ARGS_SEPARATOR } from "../constants";
@@ -101,6 +101,7 @@ type Props = {
101101
onRawModeChange?: (mode: string) => void;
102102
onInterrupt: () => void;
103103
onToggleProcessStdout?: () => void;
104+
onExitShortcut?: () => void;
104105
};
105106

106107
const PROMPT_PREFIX_WIDTH = 2;
@@ -132,9 +133,9 @@ export const PromptInput = React.memo(function PromptInput({
132133
onModelConfigChange,
133134
onInterrupt,
134135
onToggleProcessStdout,
136+
onExitShortcut,
135137
onRawModeChange,
136138
}: Props): React.ReactElement {
137-
const { exit } = useApp();
138139
const { stdout } = useStdout();
139140
const inputTextRef = useRef<DOMElement | null>(null);
140141
const [buffer, setBuffer] = useState<PromptBufferState>(EMPTY_BUFFER);
@@ -357,7 +358,7 @@ export const PromptInput = React.memo(function PromptInput({
357358
}
358359
const now = Date.now();
359360
if (pendingExit && now - lastCtrlDAt.current < 2000) {
360-
exit();
361+
onExitShortcut?.();
361362
return;
362363
}
363364
lastCtrlDAt.current = now;

packages/cli/src/utils/package.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ import { fileURLToPath } from "node:url";
33
import path from "node:path";
44
import { CLI_VERSION } from "../generated/git-commit";
55

6-
export type PackageJson = BasePackageJson & {
7-
config?: {
8-
sandboxImageUri?: string;
9-
};
10-
};
6+
export type PackageJson = BasePackageJson;
117

128
const __filename = fileURLToPath(import.meta.url);
139
const __dirname = path.dirname(__filename);

0 commit comments

Comments
 (0)