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
1,335 changes: 1,206 additions & 129 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"framer-motion": "^12.38.0",
"highlight.js": "^11.11.1",
"lucide-react": "^1.8.0",
"mermaid": "^11.17.2",
"monaco-editor": "^0.55.1",
"node-pty": "^1.1.0",
"node-ssh": "^13.2.1",
Expand Down
11 changes: 9 additions & 2 deletions resources/mcp-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ const TOOLS = [
{
name: 'submit_review',
description:
"Finish a review of this worktree's branch: a verdict plus a summary, covering every comment of yours that is still open. Leave inline comments with add_review_comment first and put cross-cutting points in the summary. Call it once per review round.\n\nIf you were opened by `request_review`, the review goes back to the tab that asked for it — you do not need to work out who that is. Otherwise it goes to the most recently active Chat tab other than yours; pass `tab` (from list_tabs) to choose. Terminal tabs cannot receive reviews. Depending on the user's settings it is delivered right away or held as a draft in the Review tab for the user to edit and send; the result says which, and whether delivery failed.",
"Finish a review of this worktree's branch: a verdict, a summary, and a briefing, covering every comment of yours that is still open. Leave inline comments with add_review_comment first and put cross-cutting points in the summary. Call it once per review round.\n\nThe summary and comments go to the author agent. The briefing does not: it is shown to the user in the Review tab, above the summary, and is written for a human who has probably not read the change yet.\n\nIf you were opened by `request_review`, the review goes back to the tab that asked for it — you do not need to work out who that is. Otherwise it goes to the most recently active Chat tab other than yours; pass `tab` (from list_tabs) to choose. Terminal tabs cannot receive reviews. Depending on the user's settings it is delivered right away or held as a draft in the Review tab for the user to edit and send; the result says which, and whether delivery failed.",
inputSchema: {
type: 'object',
properties: {
Expand All @@ -474,7 +474,13 @@ const TOOLS = [
},
summary: {
type: 'string',
description: 'Overall assessment: what the change does well, what must change, and anything not tied to one line.'
description:
'For the author agent: a short, actionable list of what must change and anything not tied to one line. Do not repeat the briefing — the user reads that, the author reads this.'
},
briefing: {
type: 'string',
description:
"Markdown for the user, not the author: the case for this change, so they can understand it and decide whether to merge without reading the diff first. Cover what it does and why, how it works (a diagram for any flow, state or data-shape change — a ```mermaid code block renders as a real diagram; mark the nodes this change touches with :::added, :::changed or :::removed, which follow the user's theme), your critique ranked by severity with blockers clearly separated from non-blocking notes, and a closing recommendation. Optional, but leave it out only for a trivial change."
},
tab: {
type: 'string',
Expand Down Expand Up @@ -1109,6 +1115,7 @@ async function handleToolCall(name, args) {
const r = await callControl('POST', '/reviews/submit', {
verdict: args.verdict,
summary: typeof args.summary === 'string' ? args.summary : '',
briefing: typeof args.briefing === 'string' && args.briefing.trim() ? args.briefing : undefined,
tab: typeof args.tab === 'string' && args.tab.trim() ? args.tab.trim() : undefined
})
const what =
Expand Down
8 changes: 7 additions & 1 deletion resources/mcp-bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,11 +546,17 @@ describe('mcp-bridge review tools', () => {
expect(draft.result.content[0].text).toMatch(/held as a draft.*tab author/)
const sent = await callTool('submit_review', { verdict: 'approve', summary: 's' })
expect(sent.result.content[0].text).toMatch(/delivered to tab author \(woke it\)/)
const failed = await callTool('submit_review', { verdict: 'comment', summary: 's', tab: 'term' })
const failed = await callTool('submit_review', {
verdict: 'comment',
summary: 's',
briefing: '## What it does',
tab: 'term'
})
expect(failed.result.content[0].text).toMatch(/NOT delivered: not supported yet/)
expect(stub.captured.filter((c) => c.url === '/reviews/submit').at(-1).body).toEqual({
verdict: 'comment',
summary: 's',
briefing: '## What it does',
tab: 'term'
})
})
Expand Down
5 changes: 4 additions & 1 deletion src/main/build-initial-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ describe('buildInitialAppState', () => {
{ id: 'c3', filePath: 'a.ts', line: 1, anchor, status: 'resolved', resolvedBy: { kind: 'robot' }, resolvedAt: 6 }
],
submissions: [
{ id: 's1', summary: '', commentIds: [], state: 'sent', verdict: 'approve', headSha: 7 }
{ id: 's1', summary: '', commentIds: [], state: 'sent', verdict: 'approve', headSha: 7, briefing: { md: 'x' } },
{ id: 's2', summary: '', commentIds: [], state: 'sent', verdict: 'approve', briefing: '## What it does' }
],
requests: [
{ id: 'r1', requester: author, reviewer: author, context: 'c', source: 'agent', reused: false, createdAt: 1 },
Expand All @@ -113,6 +114,8 @@ describe('buildInitialAppState', () => {
expect(wt.comments[2]).not.toHaveProperty('resolvedBy')
expect(wt.comments[2].resolvedAt).toBe(6)
expect(wt.submissions?.[0]).not.toHaveProperty('headSha')
expect(wt.submissions?.[0]).not.toHaveProperty('briefing')
expect(wt.submissions?.[1].briefing).toBe('## What it does')
expect(wt.requests?.map((r) => r.id)).toEqual(['r1'])
})
})
8 changes: 4 additions & 4 deletions src/main/build-initial-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ function sanitizeReviews(raw: unknown): ReviewsState['byPath'] {
(s.verdict === 'approve' || s.verdict === 'changes-requested' || s.verdict === 'comment')
)
.map((s: ReviewSubmission) => {
if (s.headSha === undefined || typeof s.headSha === 'string') return s
const { headSha: _drop, ...rest } = s
void _drop
return rest
const next = { ...s }
if (next.headSha !== undefined && typeof next.headSha !== 'string') delete next.headSha
if (next.briefing !== undefined && typeof next.briefing !== 'string') delete next.briefing
return next
})
: []
const requests: ReviewRequest[] = Array.isArray(rawRequests)
Expand Down
10 changes: 8 additions & 2 deletions src/main/control-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export interface ReviewQueries {
submit: (
worktreePath: string,
callerTabId: string,
input: { verdict: ReviewVerdict; summary: string; targetTabId?: string }
input: { verdict: ReviewVerdict; summary: string; briefing?: string; targetTabId?: string }
) => SubmitReviewResult | Promise<SubmitReviewResult>
/** Open a reviewer Chat tab for the caller and hand it the request. */
request: (
Expand Down Expand Up @@ -1046,8 +1046,14 @@ async function handleRequest(
})
}
const summary = typeof body.summary === 'string' ? body.summary : ''
const briefing = typeof body.briefing === 'string' ? body.briefing : undefined
const tab = typeof body.tab === 'string' && body.tab.trim() ? body.tab.trim() : undefined
const result = await deps.reviews.submit(wt, terminalId, { verdict, summary, targetTabId: tab })
const result = await deps.reviews.submit(wt, terminalId, {
verdict,
summary,
briefing,
targetTabId: tab
})
if ('error' in result) return sendJson(res, 400, { error: result.error })
const { submission, delivery } = result
return sendJson(res, 200, {
Expand Down
2 changes: 2 additions & 0 deletions src/main/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,8 @@ Prefer these over blind curl/fetch — or shelling out to \`open <url>\`, which

Show your work with screenshots — this applies in Chat tabs, whose transcript is rendered markdown; a terminal tab is an xterm and will show the markdown as literal text. Every screenshot_tab result comes back with the path it was saved to, and embedding an absolute image path as markdown (\`![caption](/path/to/shot.png)\`) renders it inline — so when you've changed something visual, end your reply with the screenshot that proves it rather than only describing it. This works for any image file on this machine, not just screenshot_tab captures. Append a \`#w=\` fragment to set the displayed width: \`![caption](path#w=500)\` is 500px, \`#w=50%\` fills half the column, \`#w=400x300\` sets both dimensions, and no fragment means a default size. Use the fragment rather than a \`|\` size spec in the alt text — inside a markdown table a pipe is a column delimiter, so the cell splits and the embed renders as broken literal text. Be selective: embed the one or two shots that show the result, not every frame you took while finding your way around.

Chat tabs also draw diagrams: a fenced \`\`\`mermaid code block (flowchart, sequenceDiagram, stateDiagram-v2, classDiagram, erDiagram, …) renders as a real diagram, with a toggle to its source. Reach for one when a flow, a sequence of calls, a state machine or an architecture is easier seen than read; a small ASCII sketch is still fine for something tiny. Quote node labels that contain punctuation (\`A["parse()"]\`) — a syntax error falls back to showing the source. To color nodes, use Ness's theme-aware classes instead of hex colors, which you can't check against the user's theme: \`A:::added\` (green), \`A:::changed\` (amber), \`A:::removed\` (red, struck through), \`A:::focus\` (accent) — in flowcharts, state and class diagrams. Use them to mean something, like which parts a change touches, not for decoration. In a terminal tab it is just text.

Ness also exposes shell tabs for long-running processes — anything that wouldn't naturally exit within a few seconds (dev servers, watchers, \`tail -f\`, REPL-style tools, long builds). Drive them via the ness-control shell tools (scoped to this worktree only):

- create_shell: spawn a shell tab, optionally with a command to run (\`zsh -ilc <command>\`). Returns an id — keep it for later reads.
Expand Down
21 changes: 21 additions & 0 deletions src/main/review-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,27 @@ describe('submitReview', () => {
expect(store.getSnapshot().state.reviews.byPath[WT].submissions![0].state).toBe('sent')
})

it('keeps the briefing on the submission but out of the delivered message', () => {
const { store, deps, deliver } = setup([comment('mine')])
const result = submitReview(deps, WT, author, {
verdict: 'approve',
summary: 'lgtm',
briefing: ' ## What it does\nAdds a cache. ',
holdAsDraft: false
})
if ('error' in result) throw new Error(result.error)
expect(store.getSnapshot().state.reviews.byPath[WT].submissions![0].briefing).toBe(
'## What it does\nAdds a cache.'
)
expect(deliver.mock.calls[0][2]).not.toContain('Adds a cache')
})

it('omits a blank briefing', () => {
const { store, deps } = setup([comment('mine')])
submitReview(deps, WT, author, { verdict: 'comment', summary: 's', briefing: ' ', holdAsDraft: true })
expect(store.getSnapshot().state.reviews.byPath[WT].submissions![0]).not.toHaveProperty('briefing')
})

it('records a failed delivery with a readable reason', () => {
const { store, deps } = setup([comment('mine')], () => ({
ok: false,
Expand Down
4 changes: 4 additions & 0 deletions src/main/review-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ export function requestReview(
export interface SubmitReviewInput {
verdict: ReviewVerdict
summary: string
/** Explanation for the user; shown in the Review tab, never delivered. */
briefing?: string
/** Explicit author tab. Must be a Chat tab in the worktree other than
* the reviewer's. */
targetTabId?: string
Expand Down Expand Up @@ -385,11 +387,13 @@ export function submitReview(
const requester = reviewRequester(state, worktreePath, reviewerTabId)
const targetTabId =
input.targetTabId ?? requester ?? pickReviewTarget(tabs, reviewerTabId) ?? undefined
const briefing = input.briefing?.trim()
const submission: ReviewSubmission = {
id: randomUUID(),
author,
verdict: input.verdict,
summary: input.summary.trim(),
...(briefing ? { briefing } : {}),
commentIds,
createdAt: Date.now(),
state: 'draft',
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/components/JsonModeChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import ReactMarkdown, { defaultUrlTransform, type Components } from 'react-markd
import { rehypeHighlightShared } from '../rehype-highlight-shared'
import remarkGfm from 'remark-gfm'
import { remarkInsight } from '../remark-insight'
import { remarkMermaid } from '../remark-mermaid'
import { InsightCard } from './InsightCard'
import { MarkdownPre } from './MermaidDiagram'
import {
AlertOctagon,
AlertTriangle,
Expand Down Expand Up @@ -95,7 +97,8 @@ import {
useFindController
} from './JsonModeChatFind'

const REMARK_PLUGINS = [remarkGfm, remarkInsight]
// remarkMermaid must run after remarkInsight, which rebuilds the tree.
const REMARK_PLUGINS = [remarkGfm, remarkInsight, remarkMermaid]
const REHYPE_PLUGINS = [rehypeHighlightShared, rehypeColorHex]

/** Markdown `![alt](…)` in a message. An agent that just took a browser
Expand Down Expand Up @@ -160,7 +163,8 @@ function MarkdownImage({
// <insight-card> element.
const MARKDOWN_COMPONENTS = {
'insight-card': InsightCard,
img: MarkdownImage
img: MarkdownImage,
pre: MarkdownPre
} as unknown as Components

// react-markdown's default transform blanks any src whose protocol isn't
Expand Down
Loading
Loading