Cross-tool rules + MCP config parity: Codeep now reads
AGENTS.md(Claude Code / Cursor / Kilo Code standard) as a third project-rules source, and.mcp.jsonat the workspace root as a fourth MCP config source — so users coming from those tools don't have to duplicate config.
Added
-
New provider models. Refreshed the catalog for the current release:
OpenAI's GPT-5.6 family —gpt-5.6-sol(new default, $5/$30 per MTok,
1.05M context),gpt-5.6-terra($2.50/$15), andgpt-5.6-luna($1/$6);
xAIgrok-4.5(flagship reasoning, $2/$6, 500K context); Google
gemini-3.1-flash-lite(low-latency workhorse, $0.25/$1.50, ~1.05M
context); andqwen3.7-max(replacesqwen3-max, $2.50/$7.50, 1M
context) across all four Qwen variants. Model lists, the OpenRouter seed,
reasoning-effort gates, and thetokenTrackercontext + pricing tables
(src/config/providers.ts,src/utils/tokenTracker.ts) were updated in
lockstep, so cost and context-window estimates are correct for each id. -
Cross-device session resume (
/cloud). The CLI can now pull sessions
synced from other devices (Mac app, VS Code, another CLI machine) and
resume them locally.src/utils/codeepCloud.tsgained
listCloudSessions(projectId?)andpullCloudSession(sessionId)—
thinx-sync-token-authenticated wrappers over the existing
GET /api/sessionsendpoint (which already served the web dashboard).
The new/cloudslash command lists remote sessions scoped to the
current project (or all if none is open), and on selection fetches the
full message array, persists it into the local.codeep/sessions/
store viasaveSession, and loads it — so the resumed session is
first-class: it shows up in/sessions, autosaves on the next change,
and re-syncs on the next turn.Why: the push path (
syncSession→POST /api/sessions) has existed
since 2.0, and the Mac app and web dashboard already read from the
cloud store. The CLI was the only surface that could push but not
pull, so cross-device resume — start on the desktop, continue on the
laptop — was impossible from the terminal. Now it isn't.telemetryis deliberately NOT consulted on the pull path: reading
your own previously-pushed data back is not telemetry, and the user
is explicitly asking for it. The original push was already gated. -
AGENTS.mdproject-rules support (closes #3).loadProjectRules
(src/utils/agentChat.ts) now falls back toAGENTS.mdwhen neither
.codeep/rules.mdnorCODEEP.mdexists. Lookup precedence (highest
first):.codeep/rules.md→CODEEP.md→AGENTS.md. First non-empty
file wins; they are not concatenated, so a user can keep a trimmed
AGENTS.mdfor the other tools and a richer Codeep-specific rules file.Why:
AGENTS.mdis becoming the de-facto cross-tool project-rules
standard. Users coming from Claude Code, Cursor, or Kilo Code can now
point Codeep at their existing file without duplicating rules into a
Codeep-specific file. -
.mcp.jsonMCP config source (closes #5).loadMcpServerConfig
(src/utils/mcpConfig.ts) now reads.mcp.jsonat the workspace root
in addition to.codeep/mcp_servers.json. Same JSON shape, same
mcpServersmap/array form. Precedence chain (highest first):
.codeep/mcp_servers.json(project) →.mcp.json(project, cross-tool)
→~/.codeep/mcp_servers.json(global). Non-colliding server names
from all sources merge; on collisions, higher-precedence sources win.Why: Claude Code, Cursor, and Kilo Code already read
.mcp.jsonat
the repo root. Users can now keep one MCP config file for their whole
toolset. -
Web dashboard: cache-savings insight card. The main dashboard
(Codeep-web/src/app/dashboard/page.tsx) now shows a green insight
banner when the user has any cache reads: "Prompt caching saved you
~$X" with the total cache-read tokens and a circular cache-hit-ratio
gauge. Cost-by-model rows gained a ⚡ badge with per-model cache-read
counts. Brings the dashboard to parity with the macOS app's existing
"Saved $X via caching" display.The savings estimate assumes $3/1M input rate (a common flagship
rate) and a 90% discount for cache reads. It's a ballpark — the
actual billed cost is already shown viaestimated_cost. The point
is the "you avoided spending ~$X" framing, which is what users
actually want to see.
Security
- Workspace MCP servers now require a one-time trust approval. MCP
servers defined by files that travel WITH a repo —.codeep/mcp_servers.json
and the new.mcp.json— no longer auto-spawn at startup: cloning a
repository containing one of these files would otherwise execute
repo-author-chosen commands on your machine before you typed anything.
The TUI now shows the server list and asks once per workspace ("Trust &
start"); ACP sessions (Zed, VS Code) skip untrusted workspace servers with
a notice. Manage with/mcp trust//mcp untrust(mirrors the existing
per-workspace hooks trust). Global~/.codeepservers and ACP-provided
servers are your own config and start without a prompt. /cloudhardening. Cloud API responses are now shape-validated before
use (malformed payloads degrade to the normal "unavailable" path instead of
throwing mid-picker), and the pulled session id is whitelisted before being
used as a local filename — a hostile or corrupted server response could
otherwise write outside.codeep/sessions/.- Project rules are capped at 64KB when injected into the system prompt
(applies to.codeep/rules.md,CODEEP.md, and the newAGENTS.md), so an
oversized rules file can't bloat every request or overflow small-context
models.
Fixed
/cloudresume left session identity half-updated. The pulled session
now also updatesconfig.currentSessionId(autosave and agent-mode sync
previously kept writing under the PREVIOUS session's id) and the session
display name (the next sync would have renamed the cloud record to the old
session's title). If a local copy of the pulled session is newer than the
cloud record, the local copy is loaded instead of being overwritten./cloudcross-device listing actually finds your sessions now. Scope
is a hash of the local absolute project path, so the same repo cloned at
a different path on another machine (the normal cross-device case) produced
an empty list. When the project-scoped list is empty,/cloudnow falls
back to listing all sessions; when it's non-empty it appends a "Show all
cloud sessions…" entry so a differently-pathed session is still reachable..mcp.jsonnow expands${VAR}/${VAR:-default}env references
(all MCP config files do), matching Claude Code semantics — previously the
literal"${GITHUB_TOKEN}"string was passed through and even shadowed a
real environment variable of the same name at spawn time.- Truncated action targets show the
...marker again — the path
shortener lost it for very long paths during theActionFormatting
extraction. - Lifecycle hooks degrade gracefully on Windows without a POSIX shell
(closes #4)..shhooks can't be spawned directly on Windows; previously a
pre_tool_call/pre_commithook would fail to spawn and — because those
events are blocking — could wedge every tool call. Codeep now detects the
platform's shell: it runs.shhooks through Git Bash'sshwhen installed,
and when no POSIX shell is found reports an explicit "cannot run on this
system" state in/hooksand the welcome banner and skips them (never
blocking). A new README "Windows notes" section documents which features need
a shell (hooks) and which are Node-native (rules, commands, skills, MCP config).
Breaking (Linux / Windows)
- Stored API keys need a one-time re-login after the keytar →
@napi-rs/keyringmigration (closes #6). The two libraries use different
credential-store naming on Linux (Secret Service attributes) and Windows
(Credential Vault target names), so keys saved by Codeep ≤ 2.14 are not
visible to the new backend there. The CLI detects this and prints a
one-time hint; re-add keys with/login <provider> <key>. macOS is
unaffected (both use the same Keychain items). Keychain access now also
runs off the event loop (AsyncEntry), so the TUI no longer freezes while
macOS shows a keychain-authorization dialog.
Changed
-
CLI:
App.tsmessage-formatting extracted intoMessageFormatter.ts.
The 3.2k-lineApp.tshad five pure (or near-pure) formatting methods
inlined asprivate—wordWrap,applyInlineMarkdown,formatTextLines,
formatCodeBlock,formatMessage— totalling ~600 LoC. Pulled them out
intosrc/renderer/components/MessageFormatter.ts(443 LoC) as
module-level functions, following the samecomponents/pattern already
used bySettings,Export,Search, etc.The only non-trivial change:
codeBlockCounter(previously aprivate numberonApp) is now aBlockCounterobject ({ current: number })
threaded throughformatMessageby reference, so the counter still
increments across consecutive calls during a single render pass — block
numbers for/copy [n]stay stable.Also removed a duplicate
wordWrap: the chat-flavoured variant in
App.ts(hard-breaks over-width words like long file paths) is now the
one inMessageFormatter.ts; the simplerwordWrapinansi.ts
(lets over-width words overflow) stays for non-chat use cases.Why:
App.tswas the largest TypeScript file in the project. The
formatting logic was untestable in isolation (it lived on a class that
requires a screen, input loop, and 30+ state fields to instantiate).
Extracted as pure functions, it now has 26 unit tests covering inline
markdown, word-wrap, headings, lists, blockquotes, code blocks, and
counter threading — the first tests ever for this code. App.ts shrank
from 3189 → 2860 LoC (-10%). -
ACP registry entry synced to 2.14.0.
acp-registry/codeep/agent.json
had fallen behind to 2.4.2 — ten minor releases out of date — so ACP
clients (Zed, Cursor, etc. via the registry) were advertising a stale
binary. Bumped theversionfield and all four platform archive URLs
(darwin-aarch64, darwin-x86_64, linux-x86_64, linux-aarch64) to the
v2.14.0GitHub release. -
CLI: commands.ts refactoring — extracted helpers module, 2 bugs fixed, +2082 tests total.
Decomposedcommands.ts(2488 → 2448 LoC, −40) by extracting pure
helpers from the giant switch/case into a new
renderer/commands/helpers.tsmodule (140 LoC, 49 tests):buildSearchSnippets— the asymmetric 30/50-char window snippet
extractor from/search(kept separate fromutils/search.ts’s
50/50 because the inline panel is narrower). 10 tests.parseKeepRecent— the/compact <n>arg parser.
Fixed a real bug: the oldparseInt(arg) || fallbacktreated0
as falsy and returned the default (4) instead of clamping to 2.
Switched toNumber.isNaNcheck. 7 tests.joinSessionName— the hyphen-joiner from/rename. 6 tests.parseTaskAddArgs— the--bug/--feature/--descflag parser
from/tasks add. 13 tests.formatTaskList— the Markdown list renderer from/tasks. 13 tests.
Thecommands.tscases now delegate to these helpers; the parser and
renderer have direct coverage for edge cases (empty args, flag
ordering, type overrides, null descriptions).
-
CLI: App.ts refactoring round 3 — progress bar, paste detection, notification truncation extracted; 1 bug fixed.
Continued decomposingApp.ts(2622 → 2613 LoC, −9 more) by pulling
three more pure concerns intorenderer/layout.ts:agentProgressBar— the gradient ░▒▓█ bar from
renderInlineAgentProgress. Fixed a real bug: whenmaxIterations
was 0, the old code divided by zero (yielding Infinity) and rendered a
full bar instead of an empty one. Added amaxIterations > 0guard.
6 new tests, including the zero-budget edge case.truncateNotification— the ellipsis truncation from
renderStatusBar. 7 new tests.shouldShowPasteDialog/buildPasteInfo— the paste-size threshold
check and preview struct builder extracted fromhandlePaste.
14 new tests covering the exact 100-char boundary, the>3lines rule,
and the 200-char preview truncation.
The layout module is now 123 tests strong and has uncovered one bug.
-
CLI: App.ts refactoring round 2 — input display + scroll window extracted, +2006 tests total.
Continued decomposingApp.ts(2662 → 2632 LoC, −30 more) by pulling
two more pure concerns intorenderer/layout.ts:computeInputDisplay/inputPromptSymbol/inputViewport— the
input-row geometry: prompt-symbol scaling for multi-line content,
horizontal viewport scrolling for long lines (cursor anchored at 70%
of the available width), placeholder selection. 21 new tests.scrollWindow— the visible-window math fromgetVisibleMessages
(maxScroll clamping, startIndex/endIndex derivation). 7 new tests.
renderInputandgetVisibleMessagesnow delegate to these helpers;
the layout module is up to 96 tests covering every extracted helper.
-
CLI: App.ts refactoring — extracted pure layout/input modules, +1978 tests total.
DecomposedApp.ts(2727 → 2662 LoC, −65) by pulling three previously
inlined, untestable concerns into pure, unit-tested modules:- New module
renderer/layout.ts(190 LoC, 68 tests) — the layout math
that was buried insiderenderChat,scrollToMessage,renderStatusBar,
andhandleChatKey:bottomPanelHeight(snapshot)— the 15-branch if/else that reserves
space for whichever inline panel is open (paste / agent / permission /
session picker / confirm / status / help / search / export / logout /
login / menu / settings / autocomplete), with caps and priority.chatLayout(height, panel)— derives the y-coordinates for the
messages / separator / input / status rows.messageLineCount,messageOffsets,scrollOffsetForTarget— the
word-wrap line-counting and scroll-centring logic that used to live in
scrollToMessage.formatTokenCount— the "1.2K tok" compaction.statusBarRightHint— context-sensitive right-edge hint (idle /
streaming / "new messages below" badge).activePanel(state)— the focus-precedence decision the keystroke
handler walked as a 13-step if/else chain on every keypress.
- New module
renderer/inputParsing.ts(14 tests) —
parseCommandInputextracts the/command arg argparser from
handleCommand, with whitespace collapsing and case normalisation.
App.tsnow delegates to these modules via small snapshot objects; the
big methods shrank by 65 lines and their core logic is now covered by
82 dedicated tests instead of being unreachable from the test suite. - New module
-
CLI: deeper coverage round 5 — 5 more test files, +50 tests.
Closed the remaining small-module gaps:utils/search.test.ts(15 tests) —searchMessages:
case-insensitive match (both directions), messageIndex tracking,
one-result-per-message, 50-char context window, leading/trailing
ellipsis boundary, original-case preservation, multi-word terms,
regex-char-as-literal safety.utils/commandIndex.test.ts(8 tests) —COMMAND_INDEX
structural invariants (non-empty, no dupes, slash-prefixed) and
formatCommandIndexMarkdown rendering (bullet list, backtick
wrapping, em-dash separator, one-bullet-per-command).utils/taskContext.test.ts(11 tests) —getTaskContextPrompt
empty-state, task title/description/project/badge rendering,
missing-field fallbacks, multi-task bullet list, header + footer.utils/ollamaCatalog.test.ts(10 tests) —OLLAMA_CODING_MODELS
catalog invariants (fields populated, pull-tag format, no dupes,
size variety) andcatalogAgentHint(7B threshold, consistency
with the catalog).renderer/components/uiConstants.test.ts(6 tests) —
PRIMARY_COLORANSI escape,SPINNER_FRAMES(8 distinct single
chars),LOGO_LINES/LOGO_HEIGHTconsistency.
-
CLI: deeper coverage round 4 — 6 more test files, +83 tests, 1 duplikat helpera istaknut.
renderer/main.test.ts(10 tests) —deriveSessionName(whitespace
collapse, 5-word cap, 48-char truncation with ellipsis).utils/export.test.ts(25 tests) — Markdown / JSON / plain-text
exporters: header structure, role labels, message preservation,
session name defaulting, message-count field, ISO timestamp,
format dispatch + unknown-format error, empty-list safety.renderer/components/Box.test.ts(20 tests) —boxChars
completeness,createBox(line count, y-placement, corner chars per
style, middle-row pattern, x-padding, title embed/alignment/
truncation/narrow-box suppression),centerBox(centring, floor,
negative when overflow).renderer/components/Search.test.ts(7 tests) —handleSearchKey
(escape/close, up/down clamping, enter-selects-messageIndex,
empty-results guard, ignore-unknown, position vs index).renderer/components/Intro.test.ts(12 tests) —GLITCH_CHARS,
generateNoiseLine(length, space preservation, glitch membership),
getDecryptedLine(length, space preservation, full reveal at
progress > 0.95, probabilistic reveal at 0, empty + long input).renderer/components/Logout.test.ts(9 tests) —handleLogoutKey
(escape close, up/down navigation capped at providers + all + cancel,
enter on provider / "all" / "cancel" slot, no-callback safety,
unknown-key ignore, empty-providers cap).- Also surfaced a duplicate
truncatePathhelper (identical
implementation inPermission.tsandStatus.ts) — exported both for
testability; a future refactor should consolidate into a shared util.
-
CLI: deeper coverage round 3 — 4 more test files, +79 tests.
Continued closing the test gap:utils/terminal.test.ts(31 tests) — terminal control helpers:
supportsSynchronizedOutput(env-driven detection of ghostty /
iterm / kitty / wezterm / vscode / alacritty + xterm/256color
fallback),hideCursor/showCursorescape sequences,clearLinesAbove
multi-line clear-and-restore,moveCursor(up/down),getTerminalSize
fallback, andcreateSyncWriter(sync-frame wrapping, idempotent
startSync, unsupported-terminal pass-through).utils/skillBundles.test.ts(32 tests) — the skill-bundle engine:
parseFrontmatter(BOM/CRLF normalisation, inline + block lists,
quote stripping, kebab-case keys, body preservation, no-frontmatter
fallback),stripQuotes,asStringArray,formatBundlesForSysprompt
(per-line 200-char cap, 3-trigger hint, total 4000-char budget with
"omitted" note),formatBundleList(project/global grouping, version
badge, empty hint).utils/skillBundlesCloud.test.ts(14 tests) —serialiseSkillMd
(YAML frontmatter round-trip for all optional fields and list
sections),readRawSkillMd(missing-file null),uninstallLocalBundle
(delete + idempotent second call).renderer/components/Permission.test.ts(9 tests) —
getPermissionOptions,truncatePath(basename fallback, .../ prefix,
boundary behaviour).renderer/components/SelectScreen.test.ts(12 tests) —
handleSelectKeydispatch for all navigation keys, mutation-free
state, and the defensive empty-list case.
-
CLI: deeper coverage round 2 — 4 more test files, +117 tests, 1 crash bug fixed.
Continued closing the test gap on large untested modules:utils/codeepCloud.test.ts(23 tests) —generateProjectId
(deterministic sha256-16, trailing-slash + case normalisation),
_readFileBundleForTest/_writeFileBundleForTest(the
personality/command bundle sync): case-folding of filenames,
name-regex validation, 64-char name cap, 64 KB size cap (boundary
included), additive-merge (no clobber of local edits), directory
auto-creation, empty/invalid-body skip.renderer/Input.test.ts(35 tests) —LineEditor, the
readline-style input editor: cursor movement, word-boundary
navigation (wordLeft/wordRightover spaces, path separators and
dots),deleteWordBackward(path-aware),deleteToEnd, history
navigation (up/down walk, draft restoration, empty-entry skip,
100-entry cap), andhandleKeydispatch (backspace/delete/left/
right/home/end/ctrl-left/ctrl-right/alt+b/alt+f, regular char
insert, ctrl-suppression).renderer/Screen.test.ts(16 tests) — the terminal screen
buffer: out-of-bounds rejection, newline-truncation inwrite,
writeLineoverwrite,writeWrappedline counting,horizontalLine,
cursor API, and render/fullRender safety.renderer/agentExecution.test.ts(43 tests) —getActionType
(tool-name → action classification) andisDangerousTool(name +
command heuristics). Found and fixed a real crash bug: when the
LLM returnedparameters.commandas a number (123) instead of a
string,isDangerousToolthrewTypeError: command.toLowerCase is not a function— taking the whole agent loop down with it. The fix
guards withtypeof rawCommand === 'string'.
-
CLI: deep test coverage — 6 new test files, +126 tests.
Added tests for six previously-untested modules across the CLI:config/index.test.ts(26 tests) — project detection (isProjectDirectory,
hasStandardProjectMarkers,initializeAsProject,isManuallyInitializedProject):
all 9 standard markers, the.codeep/project.jsonmanual marker,
idempotent re-initialisation, non-writable-path failure, and the
isProjectDirectoryprecedence (manual marker wins over standard markers).utils/taskPlanner.test.ts(19 tests) — the dependency-aware task
scheduler (canStartTask,getNextTask,formatTaskPlan): blocked /
unblocked dependency chains, failed-dependency handling, multi-level
chains, in-progress skip, and the status-icon / dependency rendering.utils/logger.test.ts(9 tests) —formatLogEntry: level uppercasing,
JSON-serialised data suffix, falsy-data suppression (0/false/''),
multi-line messages, trailing newline.utils/interactive.test.ts(28 tests) — the prompt-clarification
flow (analyzeForClarification,formatQuestions,parseAnswers,
enhancePromptWithAnswers): all ambiguity triggers (auth, database,
api, deploy, test, styling, state-management, refactor, form),
letter-answer parsing (1a/1b), option-name matching, and the
"proceed" escape hatch. Documents a real false-positive bug in
checkForDetails(substring match: "authentication" contains "auth"
from the "Basic auth" option label).utils/mcpMarketplace.test.ts(14 tests) —findMarketplaceEntry,
formatMarketplaceList,formatMarketplaceEntry: case-insensitive
lookup, the Markdown table rendering, one-row-per-entry invariant,
arg-hint / env-note / docs-link sections.renderer/handlers.test.ts(30 tests) — the inline modal key
handlers (handleInlineStatusKey,handleInlineHelpKey,
handleInlinePermissionKey,handleInlineSessionPickerKey,
handleInlineConfirmKey): close-on-escape, cursor clamping, scroll
step sizes, the yes/no/extra cycle, delete-mode toggling, and
empty-list cleanup.
-
CLI:
App.ts+commands.tsdecomposition — +61 tests, 2 more helpers extracted.
Continued theApp.ts/commands.tscleanup:components/Autocomplete.ts—filterCommands(value, commands),
the/-command prefix matcher. Rules: only triggers on input
starting with/and containing no space, case-insensitive prefix
match, 8-item cap. 10 tests cover the accept/reject paths, the cap,
and the empty-query special case.ollamaHint.ts—ollamaModelHint(modelId), the parameter-
count hint (✓ agent mode/⚠ chat only (< 7B)) shown beside
Ollama models. 20 tests cover the size-threshold boundary (7B),
namespaced ids (qwen3:14b), hyphenated ids (mistral-7b-instruct),
and the no-size-detected fallback.utils/toolParsing.test.ts(31 tests) — the LLM tool-call
parser had no test coverage despite being critical path (it's
how the agent extractsread_file/write_file/execute_command
calls from model output). Added test seams (_forTestexports) for
the three file-private helpers (extractPartialToolParams,
tryExtractParams,tryParseToolCall) and covered: name
normalisation (camelCase → snake_case), OpenAI / Anthropic / text
response formats, the truncation-recovery path (partial JSON →
recovered params), required-field validation (write_file missing
path, edit_file missing old_text/new_text), and the trailing-comma
tolerance intryParseToolCall.
-
VS Code:
extension.tsdecomposed — 654 → 627 LoC, +50 tests (81 → 131).
extension.ts(the activation entry point) had no test coverage and
kept its status-bar presentation rules and input-box validators inline.
Two pure-function modules extracted:statusBarRenderer.ts—renderStatusBar(state)returns the
text / tooltip / command / background-colour per connection state
(connecting / connected / reconnecting / disconnected / failed).
Theactivate()body now has a 6-lineapplyStatusBarwrapper that
applies those fields to the liveStatusBarItem. 17 tests cover each
state's icon, label, click command, tooltip content (model name in
connected, attempt counter in reconnecting), and background tint
(warning vs. error).validators.ts—validateMcpServerName,validateSkillBundleName,
validateApiKey,validateModelId,validateRequired. Five regex /
length rules that were inline asInputBox.validateInputlambdas,
now reusable and unit-tested. 33 tests cover accept paths, reject
paths, boundary lengths, leading-character rules, whitespace
trimming, and the embedded-whitespace API-key check.
-
Web: dashboard formatting helpers de-duplicated across 8 files + 24 tests.
Six pure helpers (COLORS,colorFor,maskKey,timeAgo,fmt,
fmtCost,fmtTokens) were copy-pasted — sometimes with subtly
different bodies — across eight dashboard pages and components:File Had app/dashboard/page.tsxall six app/dashboard/KeysSection.tsxtimeAgoapp/dashboard/ProjectRow.tsxtimeAgoapp/dashboard/CliConnectionSection.tsxtimeAgoapp/dashboard/projects/[name]/page.tsxtimeAgo,fmtCost,fmtTokensapp/dashboard/sessions/page.tsxtimeAgoapp/dashboard/sessions/[id]/page.tsxfmtTokensapp/dashboard/reviews/page.tsxtimeAgo,fmtapp/dashboard/reviews/CiTokensSection.tsxtimeAgoConsolidated into
src/lib/format.ts(78 LoC, single source of truth),
withfmtCost'sNumber(n) || 0NaN-guard propagated everywhere (the
project-page copy lacked it). All eight call sites now import from
@/lib/format.New
src/lib/format.test.ts(24 tests) pins the rules — palette
wrapping, key masking,timeAgotier boundaries (mocked clock),
compact number/cost/token formats including the adaptive cost
precision that sub-cent cached-token bills depend on. -
CLI:
App.tsdecomposed — 2860 → 2732 LoC, +22 tests, 3 constants de-duplicated.
App.ts(the terminal renderer's main file) had drifted back to
2860 LoC after the earlierMessageFormatterextraction. Three more
pure-function / constant clusters extracted intocomponents/:uiConstants.ts—PRIMARY_COLOR,SPINNER_FRAMES,
LOGO_LINES,LOGO_HEIGHT. De-duplication win:PRIMARY_COLOR
was redeclared inStatus.tsandIntro.ts, andLOGO_LINESwas
copy-pasted intoIntro.ts(asLOGO). Both now import from the
single source, so the palette / logo can't drift between files.ActionFormatting.ts—getActionColor,formatActionTarget,
getActionLabel(the agent-progress-panel colour/label/path
helpers). Pure functions, now unit-tested (11 tests).WelcomeFormatter.ts—formatWelcomeMessage(the welcome-
banner DSL renderer: version header, Project/Access/Mode labels,
⚠ warnings, /help hints). Pure function, now unit-tested (11 tests).
Writing the
ActionFormattingtests surfaced a real truncation bug
informatActionTarget: when a 3+ segment path was already shortened
to.../parent/fileand still exceededmaxLen, the function
prepended another...and sliced from the end — producing
.../b/c→../b/c(dropping a dot) instead of a clean left-trim.
Now slices the short form directly so the file extension always
survives. -
Mac App: unit-test coverage expanded (+64 tests, 114 → 178) + 3 parser bugs fixed.
Four new test files cover the pure parsers and helpers behind the
MessageContent/views (the ones reinstated when the split was fixed
in Cycle #29):MarkdownTableParserTests.swift(13 tests) — GFM table splitter:
header + delimiter detection, alignment colons, pipe-less tables,
row padding, prose-before/after, single-dash delimiters.MarkdownBlocksTests.swift(19 tests) — block-level markdown:
paragraphs, ATX headings (levels 1–6 + 7-hash fallback), bullet /
numbered / task lists (with[ ]/[x]/[X]markers), blockquotes,
horizontal rules, mixed content.ParseEditDiffTests.swift(8 tests) —parseEditDiff: the
edit-file tool-output parser. Nil for non-edit output, prefix-only
guard, mixed added/removed/context ordering, leading-blank skip.CodeHighlightingTests.swift(13 tests) — the SwiftUI bridge
fromSyntaxHighlightertokens toColors. Pins the colour per
kind and verifiesattributed(_:)round-trips source verbatim.SegmentsTests.swift(11 tests) —segments(from:): the
fenced-code-block splitter drivingMessageContentView. Covers
language tags, unclosed fences (streaming), inline-backtick
non-detection, tables-inside-prose promotion.
Writing the tests surfaced three real bugs in the parsers we'd
reinstated from memory in Cycle #29, now fixed:MarkdownBlocks.parseTaskItemread the checkbox marker at the
wrong string index (off-by-one) —- [ ]and- [x]were never
recognized and fell through toparseBullet, so task items
rendered as plain bullets with the[ ]/[x]visible in the
text. Index arithmetic corrected.MarkdownTableParser.isDelimiterrequired 3+ dashes per
delimiter cell. GFM/CommonMark only require one dash, so valid
tables with short delimiters (|-|-|,|:--|:-:|) were rejected
and rendered as prose. Regex relaxed to:?-+:?.MarkdownTableParser.flushBufferdidn't trim trailing blank
lines, so an empty string or a table followed by a blank produced
a spurious empty.prosechunk. Now trims trailing blanks before
emitting.
-
Mac App:
ChatView.swiftdecomposed — 2554 → 1075 LoC (−58%).
The 2.5k-lineChatView.swiftwas the second-largest Swift file in the
project (afterAppState.swift). It bundled the main chat surface,
the privateChatDetailViewhelper, and eight independent sheet /
popover / banner sub-views behind a single file. The sub-views had no
shared state with the detail view beyond what they already received as
parameters or read from@Environment(AppState.self), so they were
pure extraction candidates.Split into eight focused files under
Views/, each carrying a header
comment documenting its origin:File LoC Contents ConfirmationSheet.swift259 tool-approval modal (Run/Skip/Cancel) TasksSheet.swift285 codeep.dev tasks panel + TaskRowUsageStatsView.swift193 token/cost popover + ModelCostRowProjectContextSheet.swift243 .codeep/context viewer +ProjectNotesSheetInsightsView.swift224 codeep.dev insights panel CheckpointsSheet.swift126 checkpoint history + rewind UI UserPromptsEditor.swift138 "My Prompts" CRUD editor ErrorBanner.swift83 error banner What's left in
ChatView.swift(1075 LoC): the top-levelChatView
(NavigationSplitView shell + sheet wiring) andChatDetailView(the
actual message list + composer + toolbar). These two are tightly
coupled and share a lot of state — further splitting would just
shuffle parameters around.private struct→struct(module-internal) on every moved type so
it stays visible toChatView.swiftacross files;private extension
companions moved with their owner. -
VS Code Extension: unit-test coverage expanded (+32 tests, 49 → 81).
The VS Code extension had 4 test files covering 49 tests, all on the
vscode-free modules (acpClient,mcpConfigFile,diffPreview,
webview/markdown).chatPanel.ts(1008 LoC) — the largest source
file — had zero tests; same forcodeActions.ts.Three new test files + one expanded one close that gap by testing the
pure functions those modules already contained, exposed for tests via
_…ForTestnamed exports (same pattern asdiffPreview.test.ts):chatPanel.test.ts(6 tests) —friendlyError, the user-facing
message formatter for CLI failure modes (timeouts, crashes, missing
binary). Covers each known failure string + passthrough for unknown.codeActions.test.ts(12 tests) —fence(code-fence builder)
andbuildPrompt(lightbulb prompt templates for explain / improve /
tests / docs / fix). Verifies each action kind produces the right
instruction and embeds the fenced block; fix-without-diagnostics
omits the problems section.webview/markdown.test.ts(+14 tests) —escapeHtmland
inline, the two security-sensitive internal helpers behind
renderMarkdown. Adds direct coverage for HTML-entity escaping
(including the"href-breakout regression) and the URL-scheme
allowlist (http(s),mailto:,vscode:pass;javascript:,
data:are stripped to label-only) independently of the
full-pipeline tests.
Mocking:
chatPanel.test.tsandcodeActions.test.tsuse
vi.mock('vscode', …)with a minimal stub (same approach the existing
diffPreview.test.tspioneered) so the modules load under Node
without the Electron extension host. -
Mac App:
AppState+MCP.swiftextracted + MessageContent build fix.
The MCP (Model Context Protocol) server lifecycle —reloadMCPServers,
trustPendingMCP,dismissPendingMCP,revokeMCPTrust,
handleMCPToolsChanged,addCatalogServer,makeMCPSampler,
reattachToolsToAllConversations— moved out of the 2.8k-line
AppState.swiftinto a dedicatedAppState+MCP.swiftextension (170 LoC),
following the same pattern as the existing+Cloud/+Git/+Permissions
/+RecentProjectssplits.The MCP-related stored properties (
mcpManager,mcpTools,
mcpResults,isReloadingMCP,didWireMCPToolsHandler,
trustedMCPProjectPaths,persistTrustedMCPProjects) were widened
fromprivate/private(set)tointernal/internal(set)so the
cross-file extension can reach them — Swift'sprivateis file-scoped.
Each carries a comment explaining why.Also fixed a build regression left over from the Ciklus #29
MessageBubbledecomposition:MarkdownTableParserandMarkdownBlocks
were referenced by the extracted sub-views (MessageContentView,
ProseView,MarkdownTableView) but never defined — the parsers had
been dropped during the split. Added them as standalone files
(MarkdownTableParser.swift115 LoC,MarkdownBlocks.swift190 LoC)
and refactored the twoForEach { switch … }blocks in
MessageContentViewandProseViewinto dedicated@ViewBuilder
helper functions to satisfy Swift's result-builder type inference
(the inline switch was producing the misleading
AccessibilityRotorContentconformance error).Why:
AppState.swiftwas the largest Swift file in the project.
Pulling MCP out cuts it to 2684 LoC (-4.5%) and groups every
MCP-related entry-point under one navigable file. The parser fix
was blocking the build outright — Xcode was failing on
MessageContentViewbefore any of the new MCP extension could compile. -
Mac App:
MessageBubble.swiftdecomposed into focused sub-components.
The 924-line view was a God Object holding the bubble shell, the markdown
segment dispatcher + parser, code-block rendering + syntax highlighting,
prose/list/heading/quote rendering, GFM table rendering, and the three
tool-execution card views (ToolCallCard,ToolResultCard,DiffBody)
plus their diff parser. Split into:Views/MessageBubble.swift(315 LoC) — the bubble shell: role icon,
content switch, hover-revealed action row, inline edit mode.Views/MessageContent/MessageContentView.swift— prose/code/table
dispatcher + the fenced-code parser.Views/MessageContent/CodeBlockView.swift— code panel + the shared
CodeHighlightingpalette.Views/MessageContent/ProseView.swift— block + inline markdown.Views/MessageContent/MarkdownTableView.swift— GFM table grid.Views/MessageContent/ToolCards.swift—ToolCallCard,
ToolResultCard,EditDiff/DiffLine/DiffBody,parseEditDiff.
Pure relocation — no behaviour change. The
privatetypes became
internalso the bubble (and the existingQuickAgentView/
ChatViewcallers) can still reach them. Xcode 16 file-system-
synchronized groups pick up the new folder automatically, so no
.pbxprojedit was needed. The iOS target keeps its own copy for
now (it has UIKit-specific rendering); the same split can be applied
there as a follow-up.Why: the monolith was the largest Swift file in the project and the
one most likely to grow further (tool-card variants, snapshot tests).
Smaller files mean smaller review diffs, Xcode Previews that target
one piece at a time, and a natural seam for the snapshot-test suite
that's still missing. -
README: replaced the stale "Upgrading from 1.x to 2.0" section.
The top-of-README upgrade note still described the 2.0.0 breaking
changes as if they were the latest migration, 14 minor releases later.
Replaced with a short "Upgrading" paragraph that points at CHANGELOG
for per-release breaking changes and mentions 2.0.0 only as the last
breaking bump. The old details (MCPclientInfoversion, optional
McpServershape) remain in the 2.0.0 changelog entry. -
Native keychain backend swapped from
keytarto@napi-rs/keyring.
src/utils/keychain.tsnow loads@napi-rs/keyring(a Rust binary
with prebuilt per-platform artifacts shipped as optionalDependencies)
instead of the deprecatedkeytaraddon. The publicSecureStorage
surface is unchanged — only the internal adapter was rewritten to
bridge the new syncEntryAPI to the async methods the rest of the
code expects.Why:
keytarpullsprebuild-install, which frequently fails to
fetch or compile on new macOS releases and ARM Linux, breaking
npm installoutright for affected users.@napi-rs/keyringhas no
build step and installs a precompiled binary for the target platform
automatically, so installs are reliable across macOS / Linux / Windows
/ FreeBSD on both x64 and arm64. -
Docs:
/docs/rulesand/docs/mcprewritten to list the new
sources and the full precedence chains. README updated in lockstep.
The old docs claimed both Codeep-native rules files were loaded and
concatenated — that was never true (first non-empty file wins), so
the rewrite also fixes that inaccuracy.