Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.0.4] - 2026-10-13

### Added

- **"Compact and resume" option in the cold-cache resume confirmation prompt.** When resuming a session with an expired prompt cache or large context (`ResumeConfirm`), you can now pick `2. Compact and resume` (between `Resume` and `Start a new conversation`) to resume the session and immediately run normal conversation compaction (`/compact`) so subsequent turns carry the compacted summary instead of the full history.

### Fixed

- **All Markdown links and URLs in `attempt_completion` and assistant responses are now clickable OSC 8 terminal hyperlinks across macOS, Linux, and Windows.** `Text` (`src/ui/primitives.tsx`) now supports an `href` prop backed by OpenTUI's native OSC 8 hyperlink attributes, and `renderMarkdown` (`src/ui/markdown.tsx`) attaches `href` links to `[label](url)` links (both the label and URL, including URLs with balanced parentheses), `<https://...>` autolinks, bare `http://` / `https://` URLs in prose, lists, quotes, bold/italic spans, inline code spans, fenced code blocks, and Markdown table cells (including long URLs that wrap across lines inside narrow table columns or inside `attempt_completion`'s bordered box). In addition to native terminal OSC 8 handling (`Cmd+Click` / `Ctrl+Click` / `Shift+Cmd+Click`), `App.tsx` resolves the rendered cell's link URL (`getLinkUrlAt`) on direct clicks when terminal mouse reporting forwards mouse events to the TUI, opening the link in the default browser while preserving drag-to-select clipboard copying. Interactive startup (`src/index.tsx`) also defaults `COLORTERM=truecolor` when unset so OpenTUI's renderer enables OSC 8 output even in terminals or multiplexers that do not export `COLORTERM`.

## [7.0.3] - 2026-10-13

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "7.0.3",
"version": "7.0.4",
"description": "OrbCode CLI — agentic coding in your terminal, by MatterAI",
"type": "module",
"bin": {
Expand Down
5 changes: 5 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ async function main(): Promise<void> {
})
const initialMode = loadSettings().theme
const initialTheme = initialMode === "light" ? LIGHT_THEME : DARK_THEME
// Ensure OpenTUI's native capability detector enables OSC 8 hyperlinks and
// 24-bit color even in terminals/multiplexers that don't export COLORTERM.
if (!process.env.COLORTERM) {
process.env.COLORTERM = "truecolor"
}
const renderer = await createCliRenderer({
targetFps: 60,
gatherStats: false,
Expand Down
52 changes: 48 additions & 4 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useSelectionHandler,
useTerminalDimensions,
} from "@opentui/react";
import { Box, Text, useApp, useInput } from "./primitives.js";
import { Box, Text, getLinkUrlAt, useApp, useInput } from "./primitives.js";
import {
useTheme,
useThemeMode,
Expand Down Expand Up @@ -472,10 +472,16 @@ export function App({
}, 2000);
}, []);

const linkClickStartRef = useRef<{ x: number; y: number } | null>(null);
const linkHoveredRef = useRef(false);

useSelectionHandler((selection) => {
try {
const text = selection.getSelectedText();
if (!text || text.trim().length === 0) return;
if (!text || text.trim().length === 0) {
renderer.clearSelection();
return;
}
// Only confirm when a clipboard mechanism actually succeeded.
if (copyToClipboard(text, renderer)) {
showToast(termCols < 40 ? "✓ Text copied" : "✓ Text copied to clipboard");
Expand Down Expand Up @@ -904,6 +910,9 @@ export function App({
reasoningBufferRef.current = "";
setStreamingReasoning("");
}
if (agentRef.current) {
setContextTokens(agentRef.current.lastContextTokens);
}
setBusy(false);
maybeFetchTitle();
refreshUsage();
Expand Down Expand Up @@ -1080,7 +1089,7 @@ export function App({
);

const resumeSession = useCallback(
(session: SessionData) => {
(session: SessionData): boolean => {
setResumableSessions(null);
setPendingResume(null);
// A session from another directory continues in that directory, so its
Expand All @@ -1095,7 +1104,7 @@ export function App({
kind: "error",
text: `Can't resume "${session.title || session.id}": its directory ${session.cwd} no longer exists.`,
});
return;
return false;
}
const oldManager = mcpManagerRef.current;
mcpManagerRef.current = null;
Expand Down Expand Up @@ -1147,6 +1156,7 @@ export function App({
kind: "info",
text: `Resumed session: ${session.title || session.id}${switchedDir ? ` · working directory is now ${session.cwd}` : ""}`,
});
return true;
},
[createAgent, clearQueue, discardAgent, pushRow, resetTranscript],
);
Expand Down Expand Up @@ -2082,6 +2092,12 @@ export function App({
resumeSession(session);
return;
}
if (decision === "compact") {
if (resumeSession(session)) {
handleCommand("/compact");
}
return;
}
handleCommand("/new");
pushRow({
kind: "info",
Expand Down Expand Up @@ -2552,6 +2568,34 @@ export function App({
position="relative"
backgroundColor={theme.background}
shouldFill
onMouseDown={(event) => {
if (event.button === 0) {
linkClickStartRef.current = { x: event.x, y: event.y };
}
}}
onMouseUp={(event) => {
if (event.button !== 0 || event.defaultPrevented) {
linkClickStartRef.current = null;
return;
}
const start = linkClickStartRef.current;
linkClickStartRef.current = null;
const isPointClick =
(start !== null && start.x === event.x && start.y === event.y) ||
event.modifiers.ctrl;
if (!isPointClick) return;
const url = getLinkUrlAt(renderer, event.x, event.y);
if (!url) return;
renderer.clearSelection();
void open(url).catch(() => showToast("✗ Could not open link"));
}}
onMouseMove={(event) => {
const hasLink = Boolean(getLinkUrlAt(renderer, event.x, event.y));
if (hasLink !== linkHoveredRef.current) {
linkHoveredRef.current = hasLink;
renderer.setMousePointer(hasLink ? "pointer" : "default");
}
}}
onMouseScroll={(event) => {
if (view !== "chat" || !event.scroll) return;
const direction = event.scroll.direction;
Expand Down
7 changes: 6 additions & 1 deletion src/ui/LoginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,12 @@ export function LoginView({ onLogin }: LoginViewProps) {
<Text color={COLORS.dim}>
Approve the &quot;Authorize OrbCode CLI&quot; dialog at:
</Text>
<Text color={COLORS.dim}> {authorizeUrl}</Text>
<Text color={COLORS.dim}>
{" "}
<Text color={COLORS.dim} underline href={authorizeUrl}>
{authorizeUrl}
</Text>
</Text>
<Text color={COLORS.dim}>Esc to cancel</Text>
</>
) : (
Expand Down
9 changes: 7 additions & 2 deletions src/ui/components/ResumeConfirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { COLORS } from "../../branding.js"
import { describeResumeCost, formatIdle, formatTokens, type ResumeCostEstimate } from "../../core/resumeCost.js"
import { PopoverBox } from "./PopoverBox.js"

export type ResumeDecision = "resume" | "new"
export type ResumeDecision = "resume" | "compact" | "new"

interface ResumeConfirmProps {
estimate: ResumeCostEstimate
Expand All @@ -17,6 +17,11 @@ interface ResumeConfirmProps {

const OPTIONS: Array<{ decision: ResumeDecision; label: string; hint?: string }> = [
{ decision: "resume", label: "Resume" },
{
decision: "compact",
label: "Compact and resume",
hint: "Summarize the conversation so later turns use less context",
},
{
decision: "new",
label: "Start a new conversation",
Expand All @@ -27,7 +32,7 @@ const OPTIONS: Array<{ decision: ResumeDecision; label: string; hint?: string }>
/**
* Shown before resuming a session whose prompt cache has expired: the first
* turn re-reads the whole context at the uncached price, so a long-idle,
* large session gets a chance to start fresh instead.
* large session gets a chance to compact or start fresh instead.
*/
export function ResumeConfirm({ estimate, estimating = false, onDecision, onCancel }: ResumeConfirmProps) {
const [selected, setSelected] = useState(0)
Expand Down
Loading
Loading