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
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "7.0.0",
"version": "7.0.1",
"description": "OrbCode CLI — agentic coding in your terminal, by MatterAI",
"type": "module",
"bin": {
Expand Down Expand Up @@ -48,7 +48,7 @@
"test:resume": "node --import tsx --test test/resume-cost.test.ts",
"test:compaction": "node --import tsx --test test/compaction.test.ts test/agent-context.test.ts test/files.test.ts",
"test:effort": "node --import tsx --test test/model-effort.test.ts test/model-persistence.test.ts",
"test:ui": "bun test test/ui-viewport.test.tsx test/session-picker.test.tsx test/rewind-picker.test.tsx test/resume-confirm.test.tsx test/model-picker.test.tsx test/effort-picker.test.tsx test/tool-group.test.tsx",
"test:ui": "bun test test/ui-viewport.test.tsx test/session-picker.test.tsx test/rewind-picker.test.tsx test/resume-confirm.test.tsx test/model-picker.test.tsx test/effort-picker.test.tsx test/tool-group.test.tsx test/markdown-table.test.tsx",
"prepublishOnly": "npm run typecheck && npm run build"
},
"dependencies": {
Expand All @@ -65,7 +65,8 @@
"pdf-parse": "^2.4.5",
"picomatch": "4.0.4",
"react": "^19.2.0",
"read-excel-file": "^9.2.0"
"read-excel-file": "^9.2.0",
"string-width": "^7.2.0"
},
"devDependencies": {
"@types/node": "^20.17.0",
Expand Down
1 change: 1 addition & 0 deletions src/core/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ export class Agent {
contextTokens: this.contextTokens,
todos: this.todos,
lastGitHead: this.lastGitHead,
gitBranch: gitOutput(this.options.cwd, "git branch --show-current") || undefined,
messages: this.messages,
transcript: this.transcript,
checkpoints: this.checkpoints,
Expand Down
9 changes: 7 additions & 2 deletions src/core/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface SessionData {
todos: string
/** Last observed Git commit, used to report commits across resumed sessions. */
lastGitHead?: string
/** Git branch at the last save, shown in the resume picker. */
gitBranch?: string
/** Size of the session file in bytes. Set by `listSessions`, never persisted. */
sizeBytes?: number
messages: OpenAI.Chat.ChatCompletionMessageParam[]
/** Exact visible TUI history. Optional for sessions written before v0.4.2. */
transcript?: SessionTranscriptEntry[]
Expand Down Expand Up @@ -108,9 +112,10 @@ export function listSessions(cwd?: string): SessionData[] {
const sessions: SessionData[] = []
for (const file of files) {
try {
const data = JSON.parse(fs.readFileSync(path.join(getSessionsDir(), file), "utf8")) as SessionData
const raw = fs.readFileSync(path.join(getSessionsDir(), file))
const data = JSON.parse(raw.toString("utf8")) as SessionData
if ((cwd === undefined || data.cwd === cwd) && Array.isArray(data.messages) && data.messages.length > 0) {
sessions.push(data)
sessions.push({ ...data, sizeBytes: raw.length })
}
} catch {
// skip unreadable session files
Expand Down
25 changes: 24 additions & 1 deletion src/tools/executors/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,30 @@ export async function readFile(args: Record<string, unknown>, context: ToolConte
}
}

/**
* Why a write/edit target is unusable, phrased so the model can recover. An
* empty file_path would otherwise resolve to the cwd and fail with EISDIR.
*/
function targetPathError(rawPath: string, filePath: string): string | undefined {
if (!rawPath.trim()) {
return "file_path is missing. The tool call arguments were likely cut off before file_path was sent (e.g. the output limit was reached mid-call). Re-issue the call with file_path set; for very large files, write a first part with file_write and append the rest with file_edit."
}
try {
if (fs.statSync(filePath).isDirectory()) {
return `${filePath} is a directory; file_path must name a file.`
}
} catch {
// Missing path: a new file.
}
return undefined
}

export async function fileWrite(args: Record<string, unknown>, context: ToolContext): Promise<ToolResult> {
const filePath = resolveWorkspacePath(context.cwd, String(args.file_path ?? ""))
const rawPath = String(args.file_path ?? "")
const filePath = resolveWorkspacePath(context.cwd, rawPath)
const content = String(args.content ?? "")
const pathError = targetPathError(rawPath, filePath)
if (pathError) return { text: `Error writing file: ${pathError}`, isError: true }
try {
context.beforeWrite?.(filePath)
fs.mkdirSync(path.dirname(filePath), { recursive: true })
Expand Down Expand Up @@ -339,6 +360,8 @@ function applyEdit(content: string, edit: EditSpec): EditOutcome {

function editOneFile(context: ToolContext, edits: EditSpec[]): string[] {
const filePath = resolveWorkspacePath(context.cwd, edits[0].file_path)
const pathError = targetPathError(String(edits[0].file_path ?? ""), filePath)
if (pathError) return edits.map(() => `FAILED: ${pathError}`)
let content: string
try {
content = fs.readFileSync(filePath, "utf8")
Expand Down
2 changes: 1 addition & 1 deletion src/tools/executors/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function webFetch(args: Record<string, unknown>, context: ToolConte
"Content-Type": "application/json",
Authorization: `Bearer ${context.token}`,
},
body: JSON.stringify({ url: targetUrl }),
body: JSON.stringify({ url: targetUrl, prompt: typeof args.prompt === "string" ? args.prompt : undefined }),
signal: AbortSignal.timeout(30_000),
})
if (!response.ok) {
Expand Down
9 changes: 7 additions & 2 deletions src/tools/schemas/web_fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default {
function: {
name: "web_fetch",
description:
"Fetch content from a URL using curl. Use this when you need to scrape or retrieve content from a web page. Returns the raw HTML/text content from the URL.",
"Fetch a web page as cleaned markdown. Long pages are cut to a budget: pass a prompt describing what you need and only the most relevant sections are returned; without one you get the top of the page.",
strict: true,
parameters: {
type: "object",
Expand All @@ -14,8 +14,13 @@ export default {
type: "string",
description: "The URL to fetch content from",
},
prompt: {
type: ["string", "null"],
description:
"What you are looking for on the page, e.g. 'rate limits for the batch API'. Null to read the page from the top",
},
},
required: ["url"],
required: ["url", "prompt"],
additionalProperties: false,
},
},
Expand Down
16 changes: 11 additions & 5 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
toolGroupHeading,
type Row,
} from "./components/rows.js";
import { markdownRowCount } from "./markdown.js";
import { LinkManager } from "./components/LinkManager.js";
import { PluginManager } from "./components/PluginManager.js";
import {
Expand Down Expand Up @@ -2215,9 +2216,6 @@ export function App({
}
return;
}
if (key.escape && busy && !pendingApproval && !pendingFollowup) {
agentRef.current?.abort();
}
if (key.tab && key.shift) {
setApprovalMode((prev) => {
const next: ApprovalMode =
Expand Down Expand Up @@ -2738,6 +2736,11 @@ export function App({
onSubmit={handleSubmit}
supportsImages={getModel(settings.model).supportsImages}
onHeightChange={setInputBoxHeight}
// Esc interrupts only once the prompt is empty; an Esc that
// clears typed text never stops the running turn.
onEscape={() => {
if (busy) agentRef.current?.abort();
}}
onDoubleEscape={() => openRewind(true)}
prefill={promptPrefill}
/>
Expand Down Expand Up @@ -2797,6 +2800,7 @@ export function App({
flexDirection="column"
alignItems="center"
justifyContent="center"
backgroundColor={theme.backdrop}
zIndex={100}
>
<Box
Expand Down Expand Up @@ -3085,7 +3089,9 @@ function estimateRowLines(row: Row, width: number): number {
);
case "assistant":
// Blank messages render nothing (see rows.tsx).
return row.text.trim() ? 1 + wrapped(`● ${row.text}`) : 0;
return row.text.trim()
? 1 + markdownRowCount(row.text, Math.max(20, w - 2), w, 2)
: 0;
case "tool": {
const heading = `${formatToolName(row.name)} ${row.summary}`;
let h = 1 + wrapped(heading);
Expand Down Expand Up @@ -3115,7 +3121,7 @@ function estimateRowLines(row: Row, width: number): number {
case "error":
return 1 + wrapped(`✗ ${row.text}`);
case "completion":
return 4 + wrapped(row.text, w - 4);
return 4 + markdownRowCount(row.text, Math.max(20, w - 4), w - 4);
default:
return 1;
}
Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/FollowupPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export function FollowupPrompt({ question, suggestions, onAnswer }: FollowupProm
type your own: {custom}
{isCustomSelected && <Text underline> </Text>}
</Text>
<Text color={COLORS.dim}>↑/↓ select · enter confirm · 1-{Math.min(suggestions.length, 9)} quick pick</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>↑/↓ select · enter confirm · 1-{Math.min(suggestions.length, 9)} quick pick</Text>
</Box>
</PopoverBox>
)
}
8 changes: 5 additions & 3 deletions src/ui/components/HookTrustPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ export function HookTrustPrompt({ cwd, commands, onDecision }: HookTrustPromptPr
))}
{extra > 0 && <Text color={COLORS.dim}> … {extra} more</Text>}
</Box>
<Text color={COLORS.dim}>
Only trust hooks from a repository you trust. (y) trust &amp; enable · (n or Enter) keep disabled
</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>
Only trust hooks from a repository you trust. (y) trust &amp; enable · (n or Enter) keep disabled
</Text>
</Box>
</Box>
)
}
9 changes: 8 additions & 1 deletion src/ui/components/InputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ interface InputBoxProps {
supportsImages: boolean
/** Reports the complete rendered height, including autocomplete popups. */
onHeightChange?: (height: number) => void
/** Esc pressed on an empty prompt (an Esc that clears the prompt never reaches this). */
onEscape?: () => void
/** Esc pressed twice in quick succession on an empty prompt. */
onDoubleEscape?: () => void
/** Replaces the prompt text whenever `id` changes (e.g. the message being edited after a rewind). */
Expand Down Expand Up @@ -150,6 +152,7 @@ export function InputBox({
onSubmit,
supportsImages,
onHeightChange,
onEscape,
onDoubleEscape,
prefill,
}: InputBoxProps) {
Expand Down Expand Up @@ -401,12 +404,16 @@ export function InputBox({
(input, key) => {
const currentValue = valueRef.current
const currentCursor = cursorRef.current
if (key.escape && (currentValue.length > 0 || attachmentsRef.current.length > 0)) {
if (
key.escape &&
(currentValue.length > 0 || attachmentsRef.current.length > 0 || pasteChipsRef.current.length > 0)
) {
clearComposer()
lastEscapeRef.current = 0
return
}
if (key.escape) {
onEscape?.()
const now = Date.now()
if (onDoubleEscape && now - lastEscapeRef.current < DOUBLE_ESCAPE_MS) {
lastEscapeRef.current = 0
Expand Down
6 changes: 4 additions & 2 deletions src/ui/components/LinkManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ export function LinkManager({ links, status, onAdd, onRemove, onClose }: LinkMan
<Text underline> </Text>
</Text>
)}
{status && <Text color={COLORS.dim}>{status}</Text>}
<Text color={COLORS.dim}>↑/↓ select · enter add/remove · d remove · esc done</Text>
<Box marginTop={1} flexDirection="column">
{status && <Text color={COLORS.dim}>{status}</Text>}
<Text color={COLORS.dim}>↑/↓ select · enter add/remove · d remove · esc done</Text>
</Box>
</PopoverBox>
)
}
4 changes: 3 additions & 1 deletion src/ui/components/McpApprovalPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export function McpApprovalPrompt({ serverNames, onApprove }: McpApprovalPromptP
{windowStart + VISIBLE_ROWS < count && (
<Text color={COLORS.dim}> ↓ {count - windowStart - VISIBLE_ROWS} more</Text>
)}
<Text color={COLORS.dim}>space toggle · enter confirm · esc reject all</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>space toggle · enter confirm · esc reject all</Text>
</Box>
</Box>
</Box>
)
Expand Down
12 changes: 8 additions & 4 deletions src/ui/components/McpMigrationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export function McpMigrationPicker({
Install Claude Code or Claude Desktop, or add servers with `orbcode
mcp add`.
</Text>
<Text color={COLORS.dim}>esc to close</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>esc to close</Text>
</Box>
</PopoverBox>
);
}
Expand Down Expand Up @@ -132,9 +134,11 @@ export function McpMigrationPicker({
{windowStart + VISIBLE_ROWS < count && (
<Text color={COLORS.dim}> ↓ {count - windowStart - VISIBLE_ROWS} more</Text>
)}
<Text color={COLORS.dim}>
space toggle · enter confirm ({checkedCount}/{count}) · esc cancel
</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>
space toggle · enter confirm ({checkedCount}/{count}) · esc cancel
</Text>
</Box>
</PopoverBox>
);
}
24 changes: 15 additions & 9 deletions src/ui/components/McpPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,9 @@ export function McpPicker({ manager, onChanged, onCancel, onDeleted }: McpPicker
<Text color={COLORS.dim}>
Add servers with: orbcode mcp add &lt;name&gt; &lt;command&gt; [args...]
</Text>
<Text color={COLORS.dim}>esc to close</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>esc to close</Text>
</Box>
</PopoverBox>
)
}
Expand All @@ -344,7 +346,9 @@ export function McpPicker({ manager, onChanged, onCancel, onDeleted }: McpPicker
{pendingConfirm.title}
</Text>
<Text>{pendingConfirm.body}</Text>
<Text color={COLORS.dim}>y confirm · n / esc cancel</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>y confirm · n / esc cancel</Text>
</Box>
</PopoverBox>
)
}
Expand Down Expand Up @@ -385,13 +389,15 @@ export function McpPicker({ manager, onChanged, onCancel, onDeleted }: McpPicker
actionSelected={actionSelected}
/>
)}
<Text color={COLORS.dim}>
{busy
? busyMessage
: actionMode
? "↑/↓ select action · enter execute · esc back"
: "↑/↓ select server · enter open actions · esc close"}
</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>
{busy
? busyMessage
: actionMode
? "↑/↓ select action · enter execute · esc back"
: "↑/↓ select server · enter open actions · esc close"}
</Text>
</Box>
</PopoverBox>
)
}
Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ export function ModelPicker({
{windowStart + VISIBLE_ROWS < models.length && (
<Text color={COLORS.dim}> ↓ {models.length - windowStart - VISIBLE_ROWS} more</Text>
)}
<Text color={COLORS.dim}>↑/↓ select{effortHint} · enter confirm · esc cancel</Text>
<Box marginTop={1}>
<Text color={COLORS.dim}>↑/↓ select{effortHint} · enter confirm · esc cancel</Text>
</Box>
</PopoverBox>
)
}
Loading
Loading