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

## [0.2.41.0] - 2026-10-25 (development candidate)

Cycle 0052 pt2 ([Issue #124](https://github.com/laststance/codebook/issues/124)), the second of three pull requests of a bug-fix cycle: a test replay's moves, notices and summary page.

**Moves show no stop you moved away from.**

- **The recorded-source tab draws no CodeLens.** The editor redraws a lens only after its own delay, so a lens could still name the stop you left beside the next moment. The stop's whole label stays at the end of the stopped line, and its hover carries it where a narrow group cuts the line.
- **A move from a stop to an output or the result** shows the placeholder from its first line, with no sticky-scroll header of the stop's source above it.
- **A move that fails is worded by its cause.** When the replay debugger cannot start, the line keeps its restart advice; when the moment fails for another cause, it reads `The recorded moment could not be shown: <cause>. The last moment is still shown. Select this stop again to retry.` The line says the last moment is still shown only when it is: when it cannot be shown again either, the recorded-source row says why.
- **A refused or failed move shows the storage folder as `<storage>`**, on the transport's status line and in the Study pane's line, also after the runtime is released; the causes Codebook words after a release name the storage, the workspace and the home folder as `<storage>`, `<workspace>` and `~`, as before it.

**Notices and the summary page.**

- **A one-replay-at-a-time notice you leave unanswered never holds up your next Open replay**, which asks its own notice at once; a later answer to the earlier notice still closes nothing.
- **Each open of a test replay starts with its question, summary and capture reason collapsed**, also when you open the same replay again; a move inside the replay keeps the text you expanded.
- **The diagram marks the selected step on that step's own arrow and label**, also in a drawing whose labels are not in step order.

**Compatibility.** The MCP API stays 1.8, the bundled skill is unchanged and no stored format changes. Tasks 5, 7 to 10, 22, 23, 26, 27 and 47 of Issue #124 follow in the cycle's third pull request.

## [0.2.40.0] - 2026-10-25 (development candidate)

Cycle 0052 pt1 ([Issue #124](https://github.com/laststance/codebook/issues/124)), the first of three pull requests of a bug-fix cycle: replays leave, open and reload without stale frames, and the entries and exits behave as you chose.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Codebook contributes `debug.focusEditorOnBreak: false` as the native debugger de

## Implementation status

This is a preview, not a Marketplace or Open VSX release. The delivered version is **0.2.40.0** once the first pull request of [Cycle 0052](https://github.com/laststance/codebook/issues/124) is merged; until then 0.2.39.0 ([PR #123](https://github.com/laststance/codebook/pull/123)) is the delivered one, and the active cycle Issue says where the delivery stands. Cycle 0051 opened a test replay in the learning-replay composition a browser replay has: the Learning summary in the chat's place, one transport for both replay families, and a Learning summary button that shows a hidden summary again. Cycle 0052 is a bug-fix cycle that closes every defect Cycle 0051 carried, in three pull requests: the first makes replays leave, open and reload without stale frames and makes their entries and exits behave as the learner chose; the second fixes a test replay's moves and its summary page; the third the live runtime's unreadable facts, a browser run's refusals and the tests' hygiene. The MCP API is 1.8. What each earlier cycle delivered, with its tests, trials and limits, is in the [change history](CHANGELOG.md) and in that cycle's record, kept under the tag [`docs-archive-2026-10-20`](https://github.com/laststance/codebook/tree/docs-archive-2026-10-20/docs/cycles); from Cycle 0048 on, the cycle's Issue holds its tasks and state, and its pull requests the results.
This is a preview, not a Marketplace or Open VSX release. The delivered version is **0.2.41.0** once the second pull request of [Cycle 0052](https://github.com/laststance/codebook/issues/124) is merged; until then 0.2.40.0 ([PR #125](https://github.com/laststance/codebook/pull/125)) is the delivered one, and the active cycle Issue says where the delivery stands. Cycle 0051 opened a test replay in the learning-replay composition a browser replay has: the Learning summary in the chat's place, one transport for both replay families, and a Learning summary button that shows a hidden summary again. Cycle 0052 is a bug-fix cycle that closes every defect Cycle 0051 carried, in three pull requests: the first makes replays leave, open and reload without stale frames and makes their entries and exits behave as the learner chose; the second fixes a test replay's moves and its summary page; the third the live runtime's unreadable facts, a browser run's refusals and the tests' hygiene. The MCP API is 1.8. What each earlier cycle delivered, with its tests, trials and limits, is in the [change history](CHANGELOG.md) and in that cycle's record, kept under the tag [`docs-archive-2026-10-20`](https://github.com/laststance/codebook/tree/docs-archive-2026-10-20/docs/cycles); from Cycle 0048 on, the cycle's Issue holds its tasks and state, and its pull requests the results.

The specification is [goal version 10](PRODUCT_GOAL.md), and the implementation targets external repositories. Historical fixture or coupled-Wiki success is not proof of that destination.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.40.0
0.2.41.0
10 changes: 5 additions & 5 deletions extension/README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "codebook",
"description": "Connect an external agent to source-grounded codebase studies.",
"publisher": "laststance",
"version": "0.2.40",
"version": "0.2.41",
"private": true,
"engines": {
"vscode": "^1.120.0",
Expand Down
74 changes: 74 additions & 0 deletions extension/scripts/study-bootstrap-qa.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10527,6 +10527,80 @@ async function check(page, options, collected) {
})
},
)
// Value: protects=the drawing marks the selected step's own arrow and label also when its nodes do not stand in step order, because the renderer names each arrow and label by its step as it draws them; fails_when=the marker pairs arrows with steps by their order and labels by nextElementSibling, so a drawing whose labels stand before their arrows marks another step's label or leaves an arrow unnamed (Cycle 0052 task 19, /ship review); why_new=the drawing-mark case drew the nodes in step order only; seam=the page's Element.prototype.append, patched in this case to append each step's label before its arrow and restored after; no_claim=native pixels
await regionTwoCase(
"the drawing marks the selected step's own arrow and label also when its labels stand before their arrows",
async () => {
await freshPane('context')
await page.evaluate(() => {
const append = Element.prototype.append
window.originalAppend = append
Element.prototype.append = function (...nodes) {
const [first, second] = nodes
// Only the drawing's arrow-and-label pairs swap: the label goes in before its arrow.
if (
this instanceof SVGSVGElement &&
nodes.length === 2 &&
first instanceof SVGPathElement &&
first.classList.contains('sequence-arrow')
)
return append.call(this, second, first)
return append.apply(this, nodes)
}
})
let marked
try {
await publish(regionTwoAt(4))
marked = await page.evaluate(() =>
Array.from(
document.querySelectorAll(
'#node-learning-sequence [aria-current="step"]',
),
).map((node) =>
node.tagName === 'text'
? `text:${node.textContent}`
: `${node.tagName}:${node.dataset.step}`,
),
)
} finally {
// The page's own append comes back also when the publish or the reading threw, so no later case draws through the patch.
await page.evaluate(() => {
Element.prototype.append = window.originalAppend
})
}
cases.push({
name: "a test replay's drawing whose labels stand before their arrows marks the selected step's own label and arrow",
actual: marked,
passed:
JSON.stringify(marked) ===
JSON.stringify(['text:2. Read the rows', 'path:rows']),
})
},
)
// Value: protects=every open of a test replay starts with its clamped texts collapsed, the same replay opened again too, while a move inside the open keeps the learner's expanded text; fails_when=the expanded state of Show more lives on in the button's aria-expanded across opens, so a later open starts expanded (Cycle 0052 task 20, /ship review); why_new=the Show more case read one open only; seam=the Study message's nodeHistoryScrollToken, which names each open; no_claim=native fonts
await regionTwoCase(
'every open of a test replay starts with its clamped texts collapsed, while a move inside the open keeps an expanded text',
async () => {
await freshPane('context', { width: 298, height: 885 })
const summaryState = async () =>
(await clampReading()).find(
(reading) => reading.id === 'node-learning-summary',
).expanded
await publish(longRegionTwo(4, { nodeHistoryScrollToken: 31 }))
await page.locator('[data-show-more="node-learning-summary"]').click()
// A move inside the open: the same open's token.
await publish(longRegionTwo(2, { nodeHistoryScrollToken: 31 }))
const afterMove = await summaryState()
// The same replay opened again: a new token.
await publish(longRegionTwo(4, { nodeHistoryScrollToken: 32 }))
const afterReopen = await summaryState()
cases.push({
name: 'a test replay opened again starts with its processing summary collapsed, and a move inside an open keeps it expanded',
actual: { afterMove, afterReopen },
passed: afterMove === 'true' && afterReopen === 'false',
})
},
)
// Value: protects=the question, the processing summary and the capture reason each clamped to three lines with Show more, which shows the whole text in place and Show less takes back; fails_when=a long text pushes the moment block down unclamped, the control is missing, or it does not expand and collapse; why_new=Cycle 0051 DS-B, the Eng review's finding (schema-length texts); seam=none; no_claim=native fonts
await regionTwoCase(
'the question, the processing summary and the capture reason each clamped to three lines with Show more, which shows the ',
Expand Down
2 changes: 1 addition & 1 deletion extension/scripts/tests/package.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ test('a real VSIX loads outside the checkout and excludes bundled applications a
// Assert
assert.equal(receipt.isolatedModuleLoad, true)
assert.equal(receipt.isolatedWikiPageWorker, true)
assert.equal(checked.extension.version, '0.2.40')
assert.equal(checked.extension.version, '0.2.41')
assert.equal(checked.extension.apiVersion, '1.8')
assert.equal(checked.entries, 32)
assert.equal(receipt.dependencies.length, 16)
Expand Down
5 changes: 2 additions & 3 deletions extension/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CAPTURE_TIMEOUT_MS } from './runtimeSupport/constants'
* 1.8 changed node-test outcomes: a passing test whose history sealed complete reads `demonstrated` in `finalOutcome`, `studyOutcome` and `recordedStudyOutcome` (1.7 stopped at `partial`), its sealed history is a test replay (`recordingAvailable` and `replayAvailable`), and `nodeTest` adds `testResult`, `learningCoverage` and `learningMissing`. A node-test start's deterministic refusals arrive as the run's `error` before its first stop, a failed history seal makes `codebook_stop` answer `HISTORY_SEAL_FAILED`, and `codebook_playback` refuses a test replay with `PLAYBACK_PROFILE_UNSUPPORTED`.
*/
export const API_VERSION = '1.8'
export const EXTENSION_VERSION = '0.2.40'
export const EXTENSION_VERSION = '0.2.41'
export const CONFIGURED_MINIMUM_EXTENSION_VERSION = '0.2.0'
export const NODE_TEST_MINIMUM_EXTENSION_VERSION = '0.2.12'
export const LOOPBACK_ADDRESS = '127.0.0.1'
Expand Down Expand Up @@ -683,8 +683,7 @@ export const NODE_SOURCE_SLOT_DIRECTORY_HASH_CHARS = 12
export const NODE_HISTORY_TRANSITION_BOUND_MS = 1_000
/**
* How long a test replay's recorded-source slot keeps the earlier stop's bytes after the selection left that stop, so no native view still drawing the earlier stop sits beside the next moment's bytes (F385, F387).
* Two native views redraw after the leave, and each repaints on a delay of its own: VS Code's Call Stack view applies the debugger's `continued` in a 50 ms scheduler (`CallStackView.onCallStackChangeScheduler`) followed by the thread and stack requests, and the editor refreshes CodeLenses `provideCodeLensDebounce` after a provider change, at least 250 ms (`LanguageFeatureDebounceService.for(codeLensProvider, 'CodeLensProvide', { min: 250 })`, 1.143.0), restarted by every content change of the document.
* 300 ms is that 250 ms plus the time to ask the provider and paint; the bytes change once, after it.
* The native view that redraws after the leave is the Call Stack: VS Code's applies the debugger's `continued` in a 50 ms scheduler (`CallStackView.onCallStackChangeScheduler`) followed by the thread and stack requests, and Cursor's is slower. The value was set for the slot's CodeLens as well, which the editor refreshed at least 250 ms after a provider change (`LanguageFeatureDebounceService.for(codeLensProvider, 'CodeLensProvide', { min: 250 })`, 1.143.0); that lens is gone (Cycle 0052 task 1), and the 300 ms that N50 and N51 verified natively are kept. The bytes change once, after it.
*/
export const NODE_HISTORY_LEAVE_SETTLE_MS = 300
/** One deadline over a test replay's whole recorded-source show: the document's open, its show, the wait for its bytes and the close of the earlier slot tab. When it passes first, the moment reports its `history-source` row and releases Play's pending selection (closure rule 7 of #118). */
Expand Down
12 changes: 12 additions & 0 deletions extension/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,18 @@ export class NodeLearningRevisionChangedError extends Error {
}
}

/**
* Marks a failure of the replay debugger's start (the editor refused or did not finish launching the historical session), so a test replay move can tell it from any other failure of the moment it presents; thrown by {@link ReplayDebugger}'s session wait and read by {@link StudyWorkspace}'s move failure sentence (Cycle 0052 task 17).
* The message is the start failure's own and the name stays `Error`, so every reader of the message or the string form sees what it saw before.
* @example throw new ReplayDebuggerStartError(new Error('The editor did not start its historical debug session.'))
*/
export class ReplayDebuggerStartError extends Error {
constructor(cause: unknown) {
// The name stays `Error`, so every line that shows the error's string form reads as before; only `instanceof` tells it apart.
super(cause instanceof Error ? cause.message : String(cause), { cause })
}
}

/** Normalizes unexpected failures without pretending they succeeded; called by {@link StudyService} where a failure becomes a run's error. The MCP answers use {@link StudyService.unexpectedFailure}, which also hides private paths in the cause. @example domainError(new Error('disk')).code */
export function domainError(error: unknown): StudyError {
return error instanceof StudyError
Expand Down
11 changes: 8 additions & 3 deletions extension/src/replayDebugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
REPLAY_DEBUG_VALUES_PAINT_ALLOWANCE_MS,
REPLAY_DEBUG_TYPE,
} from './constants'
import { ReplayDebuggerStartError } from './errors'
import {
ReplayDebugAdapter,
type ReplaySourceDocument,
Expand Down Expand Up @@ -278,7 +279,7 @@ export class ReplayDebugger {
)
}

/** Reports whether playback may still present history; false after the learner stops the owned session, so an explicit replay reopen replaces this owner. @example if (!debuggerView.available) await debuggerView.dispose() */
/** Reports whether playback may still present history; false after the learner stops the owned session or a startup or presentation bound expired, so the next explicit replay open, or an explicit selection or Inspect of a test replay's stop, replaces this owner ({@link StudyWorkspace}'s `replaceUnavailableReplayDebugger`); a seek or Play never does. @example if (!debuggerView.available) await debuggerView.dispose() */
get available(): boolean {
return !this.closed && !this.detached
}
Expand Down Expand Up @@ -754,7 +755,7 @@ export class ReplayDebugger {
* bounded by {@link bounded}, so a presentation whose signal never aborts (a saved Node stop) cannot wait forever either.
*
* @param signal - The waiting presentation's cancellation.
* @throws The signal's reason once it aborts, the startup failure (which the next presentation retries) or the closed-debugger error after expiry.
* @throws The signal's reason once it aborts; otherwise a {@link ReplayDebuggerStartError} carrying the startup failure (which the next presentation retries) or the closed-debugger error of the startup bound's expiry: the start did not finish, this owner is no longer {@link available}, and the next explicit selection replaces and starts it again, so the move's restart clause holds.
* @example await this.awaitSession(signal)
*/
private async awaitSession(signal: AbortSignal): Promise<void> {
Expand All @@ -771,7 +772,11 @@ export class ReplayDebugger {
})
this.starting = starting
}
await untilAborted(this.starting, signal)
await untilAborted(this.starting, signal).catch((error: unknown) => {
// The waiter's own cancellation is no start failure; anything else ended the start (Cycle 0052 task 17).
if (signal.aborted) throw error
throw new ReplayDebuggerStartError(error)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
Expand Down
Loading