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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 0.1.19 - 2026-06-25

### Fixed

- Distinguish explicit user cancels from transport, WiFi, lid-sleep, or connection aborts so interrupted session state no longer falsely implies the user stopped the run.
- Preserve partial shell/task output with non-user-disconnect metadata instead of labeling it as `User aborted the command`.
- Prevent Loop Workflows from completing as `0/0`, reject invalid fixed zero-turn budgets, and keep unbounded monitors open instead of storing `maxTurns: 0`.
- Keep manual and signal-driven Loop Workflows from auto-running on every scheduler tick; only interval/adaptive loops become due from `nextWakeup`.
- Let implementation loops run with normal execution when the objective explicitly asks to write, edit, fix, implement, code, or create files, while preserving explicit report-only and custom-gated modes.
- Delete loop-owned root chat sessions when deleting a Loop Workflow and expose delete parity across legacy and raw HttpApi loop routes.
- Keep minimal/full chat presentation from clipping long fenced or text-fence lines by wrapping to the visible display width.

### Changed

- Improve Loop Workflow tool/prompt guidance so assistants inspect existing loops before recreating them, never use `maxTurns: 0`, and report invalid `completed 0/0` states instead of spawning duplicates.
- Expand Loop Workflow tool metadata for TUI cards with permission mode, trigger, budget, model, objective, agent, and timestamps.
- Add `/loops` dashboard warnings for legacy invalid zero-turn budgets.
- Preserve unfinished assistant/tool phases through compaction and keep Herdr status accurate while local tools, child sessions, and loop workflows are still active.

### Tests

- Add regression coverage for non-user abort classification in shell, task, and message error handling.
- Add Loop Workflow regressions for zero-turn budgets, due scheduling, permission inference, delete lifecycle, legacy/HttpApi route parity, and loop prompt guidance.
- Add chat presentation regressions for fenced long-line wrapping, Unicode display width, text-fence wrapping, hex color handling, and table preservation.

## 0.1.18 - 2026-06-24

### Added
Expand Down
2 changes: 1 addition & 1 deletion src/mendcode/bun.lock

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

12 changes: 6 additions & 6 deletions src/mendcode/packages/extensions/zed/extension.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
id = "mendcode"
name = "MendCode"
description = "The open source coding agent."
version = "0.1.18"
version = "0.1.19"
schema_version = 1
authors = ["MendCode"]
repository = "https://github.com/MendCode/MendCode"
Expand All @@ -11,26 +11,26 @@ name = "MendCode"
icon = "./icons/opencode.svg"

[agent_servers.mendcode.targets.darwin-aarch64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.18/mendcode-darwin-arm64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.19/mendcode-darwin-arm64.zip"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.darwin-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.18/mendcode-darwin-x64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.19/mendcode-darwin-x64.zip"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.linux-aarch64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.18/mendcode-linux-arm64.tar.gz"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.19/mendcode-linux-arm64.tar.gz"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.linux-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.18/mendcode-linux-x64.tar.gz"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.19/mendcode-linux-x64.tar.gz"
cmd = "./mendcode"
args = ["acp"]

[agent_servers.mendcode.targets.windows-x86_64]
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.18/mendcode-windows-x64.zip"
archive = "https://github.com/MendCode/MendCode/releases/download/v0.1.19/mendcode-windows-x64.zip"
cmd = "./mendcode.exe"
args = ["acp"]
2 changes: 1 addition & 1 deletion src/mendcode/packages/opencode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.1.18",
"version": "0.1.19",
"name": "mendcode",
"type": "module",
"license": "MIT",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,67 @@ export function hasStyledHexColors(content: string) {
return HEX_TEST_PATTERN.test(content)
}

function wrapDisplayLine(line: string, width: number) {
const maxWidth = Math.max(1, Math.floor(width))
if (!line || Bun.stringWidth(line) <= maxWidth) return [line]

const lines: string[] = []
let current = ""
const pushWideWord = (word: string) => {
let chunk = ""
for (const char of word) {
const next = `${chunk}${char}`
if (Bun.stringWidth(next) <= maxWidth) {
chunk = next
continue
}
if (chunk) lines.push(chunk)
chunk = char
}
return chunk
}

for (const part of line.split(/(\s+)/)) {
if (!part) continue
const next = `${current}${part}`
if (!current || Bun.stringWidth(next) <= maxWidth) {
current = next
continue
}
lines.push(current.trimEnd())
current = Bun.stringWidth(part) <= maxWidth ? part.trimStart() : pushWideWord(part.trimStart())
}
if (current) lines.push(current.trimEnd())
return lines.length ? lines : [line]
}

export function wrapMarkdownDisplayCodeBlocks(content: string, width: number | undefined) {
if (!width || width <= 0) return content
let inFence = false
const wrapped: string[] = []
for (const line of content.split("\n")) {
if (/^\s*```/.test(line)) {
inFence = !inFence
wrapped.push(line)
continue
}
wrapped.push(...(inFence ? wrapDisplayLine(line, Math.max(1, width - 1)) : [line]))
}
return wrapped.join("\n")
}

function isBoxDrawingLine(line: string) {
return /^\s*[│├┌┐└┘┬┴┼╭╮╰╯─━╞╪╡]/.test(line)
}

export function wrapPlainDisplayText(content: string, width: number | undefined) {
if (!width || width <= 0) return content
return content
.split("\n")
.flatMap((line) => (isBoxDrawingLine(line) ? [line] : wrapDisplayLine(line, Math.max(1, width - 1))))
.join("\n")
}

function foregroundFor(hex: string) {
const color = normalizeHexColor(hex)
if (!color) return RGBA.fromInts(255, 255, 255)
Expand All @@ -39,6 +100,17 @@ function foregroundFor(hex: string) {
return luminance > 0.55 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255)
}

function isMarkdownTableLine(line: string) {
const trimmed = line.trim()
return trimmed.length > 1 && trimmed.startsWith("|") && trimmed.endsWith("|")
}

export function shouldColorizeHexMarkdownLine(line: string, inFence = false) {
if (inFence) return false
if (isMarkdownTableLine(line)) return false
return hasStyledHexColors(line)
}

function HexStyledLine(props: { line: string; fallback: RGBA; colorize?: boolean }) {
const parts = createMemo(() => {
const items: Array<{ text: string; hex?: string }> = []
Expand Down Expand Up @@ -85,17 +157,19 @@ function HexStyledLine(props: { line: string; fallback: RGBA; colorize?: boolean
)
}

function HexStyledLines(props: { content: string; fallback: RGBA; colorize?: boolean }) {
function HexStyledLines(props: { content: string; fallback: RGBA; colorize?: boolean; width?: number }) {
const content = createMemo(() => wrapPlainDisplayText(props.content, props.width))
return (
<box flexDirection="column" flexShrink={0}>
<For each={props.content.split("\n")}>
<For each={content().split("\n")}>
{(line) => <HexStyledLine line={line} fallback={props.fallback} colorize={props.colorize} />}
</For>
</box>
)
}

function MarkdownSegment(props: StyledPlanMarkdownProps & { content: string }) {
const displayContent = createMemo(() => wrapMarkdownDisplayCodeBlocks(props.content, props.width))
const chunks = createMemo(() => {
const result: Array<{ kind: "markdown" | "hex"; content: string }> = []
const markdown: string[] = []
Expand All @@ -105,8 +179,14 @@ function MarkdownSegment(props: StyledPlanMarkdownProps & { content: string }) {
markdown.length = 0
}

for (const line of props.content.split("\n")) {
if (hasStyledHexColors(line)) {
let inFence = false
for (const line of displayContent().split("\n")) {
if (/^\s*```/.test(line)) {
markdown.push(line)
inFence = !inFence
continue
}
if (shouldColorizeHexMarkdownLine(line, inFence)) {
flushMarkdown()
result.push({ kind: "hex", content: line })
continue
Expand Down Expand Up @@ -156,7 +236,7 @@ export function StyledPlanMarkdown(props: StyledPlanMarkdownProps) {
{(segment) => (
<Switch>
<Match when={props.stableTextMode || segment.kind === "text"}>
<HexStyledLines content={segment.content} fallback={props.fg} colorize={props.colorizeHex} />
<HexStyledLines content={segment.content} fallback={props.fg} colorize={props.colorizeHex} width={props.width} />
</Match>
<Match when={true}>
<MarkdownSegment {...props} content={segment.content} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,18 @@ function stateLabel(workflow: Pick<LoopWorkflow, "state" | "phase">) {
return workflow.phase && workflow.phase !== "ready" ? `${workflow.state}: ${workflow.phase}` : workflow.state
}

function hasInvalidZeroBudget(workflow: LoopWorkflow) {
return workflow.policy?.maxTurns === 0
}

function progressLabel(workflow: LoopWorkflow) {
const turns = workflow.metrics?.turns ?? 0
const state = workflow.state.toLowerCase()
const phase = workflow.phase?.toLowerCase()
const running = state === "working" || phase === "executing"
const visible = running ? turns + 1 : turns
const maxTurns = workflow.policy?.maxTurns
if (maxTurns === 0) return `${visible}/invalid`
const current = typeof maxTurns === "number" ? Math.min(visible, maxTurns) : visible
return typeof maxTurns === "number" ? `${current}/${maxTurns}` : `${current}/open`
}
Expand Down Expand Up @@ -397,13 +402,14 @@ export function Loops() {
}
})

const detailRows = createMemo(() => {
const detailRows = createMemo<string[][]>(() => {
const item = detail()
if (!item) return []
now()
return [
["state", stateLabel(item)],
["iteration", progressLabel(item)],
...(hasInvalidZeroBudget(item) ? [["budget", "invalid maxTurns=0; recreate with positive cap or unlimited"]] : []),
["next", relativeWakeup(item)],
["cadence", cadenceLabel(item)],
["model", modelLabel(sync.data.provider, item, snapshot.latest?.rootSession)],
Expand Down
22 changes: 21 additions & 1 deletion src/mendcode/packages/opencode/src/mend/prompt/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,30 @@ function section(input: Omit<PromptSection, "bytes" | "preview">): PromptSection
}
}

type PromptSourceMetadata = {
sourceRepo?: string | null
sourceCommit?: string | null
copiedAt?: string | null
}

function relativePath(root: string, file: string | null) {
if (!file) return null
return file.startsWith(root) ? file.slice(root.length + 1) : file
}

function promptSourceMetadata(value: unknown): PromptSourceMetadata | null {
if (!value || typeof value !== "object") return null
return value as PromptSourceMetadata
}

function minimalBoundary() {
return [
"You are MendCode CLI. Answer the user directly.",
"Use the available terminal coding tools accurately.",
"For monitored loops or repeated autonomous iterations, use the `loop` tool; `/loop` creates/activates and `/loops` lists or shows existing workflows. Ask only for missing critical settings.",
"Before creating another loop for the same goal, list/show existing workflows; if a loop shows completed 0/0 or no next wakeup unexpectedly, report the invalid zero-budget state instead of recreating loops.",
"Never set loop maxTurns to 0. Use a positive cap, or omit maxTurns for unlimited/unbounded monitoring.",
"If the user asks the loop to write, edit, fix, implement, code, or create files, use normal execution rather than report-only; report-only is for inspection/monitoring/reporting objectives.",
"Do not claim tests, builds, provider calls, or file writes passed unless they actually ran.",
"Do not expose secrets or raw auth tokens.",
].join("\n")
Expand All @@ -100,6 +114,9 @@ function loopWorkflowBrief() {
"- Ask with the `question` tool only when objective, iteration limit, cadence, model/provider, max runtime, permissions, or stop condition are missing.",
"- Create a reviewable loop draft first, or activate directly when the objective, model, iteration limit, cadence, permissions, and stop condition are already clear.",
"- Use report-only mode unless the user explicitly allows edits; do not write `Iteration 1/5` through `Iteration 5/5` manually in the current chat turn.",
"- A user request to write, edit, fix, implement, code, or create files is explicit edit approval for that loop; create it with normal execution instead of report-only.",
"- Never use `maxTurns: 0`; fixed/max-goal loops need a positive cap, while unbounded-monitor loops should omit the cap.",
"- If a loop appears completed without runs, `completed 0/0`, or missing an expected next wakeup, inspect it with list/show and report the invalid state instead of creating replacement loops repeatedly.",
"- Report the loop id, current phase, next wakeup, and where the user can monitor it in the TUI.",
].join("\n")
}
Expand All @@ -114,7 +131,10 @@ function loopWorkflowFull() {
"- When model/provider is unspecified and it matters for cost, speed, capability, or the user's request, ask the user to choose from the configured providers/models that are visible in the session. If no choice is needed, use the current session default.",
"- Activation should create or reuse the loop root session, show it as Looping/background in Agent View, and ensure the project loop service when available.",
"- For safe tests, prefer report-only execution: the agent may read and analyze, but edit/write/shell/subagent escalation remains denied unless the user explicitly opts into normal execution.",
"- If the requested loop objective includes writing, editing, fixing, implementing, coding, or creating files, that is explicit normal-execution intent; do not downgrade it to report-only just because it is a loop.",
"- For a bounded test loop such as five directory-inspection iterations, create a loop with a 5-run cap, report-only permissions, a concise per-run diff/new-findings report, and a final summary after the fifth run.",
"- Never use a zero iteration cap. Use positive maxTurns for bounded/fixed work; omit maxTurns for unbounded-monitor cadence so scheduled loops do not complete as 0/0 before their first run.",
"- Before recreating a loop, inspect existing workflows with list/show. A loop in completed 0/0, no-runs, or missing-next-wakeup state is an invalid workflow to report or fix, not a reason to create more loops blindly.",
"- The loop service is responsible for durable wakeups after the TUI or chat session closes. SSE is a live refresh channel for open TUIs; storage is the source of truth when the TUI reopens.",
"- Prefer the `loop` tool over shell commands. If the tool is unavailable, the CLI namespace is plural `mendcode loops`; never try `mendcode loop`.",
"- Slash UX: `/loop <objective>` should produce an activate/draft flow; `/loops` should call list; `/loops <loop_id>` may call show with workflowID. For stop/pause/resume/run requests without a visible id, use the loop tool action and let it resolve the current session's contextual loop.",
Expand Down Expand Up @@ -295,7 +315,7 @@ export async function composePromptPolicy(input: ComposeInput = {}): Promise<Pro
.filter((item) => item.id !== "harness")
.map((item) => item.text)
.join("\n\n")
const metadata = harness?.metadata as any
const metadata = promptSourceMetadata(harness?.metadata)
return {
mode,
focusID,
Expand Down
Loading
Loading