Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ad61300
chore(site[mcp]) Refresh Julia's protocol snapshot to master
tony Nov 3, 2026
cb6b25b
feat(scripts[mcp-transcript]) Record Julia's MCP server
tony Nov 3, 2026
9b84284
feat(site[mcp]) Show Julia's recorded worked example
tony Nov 3, 2026
a77c3d1
test(site[mcp-replay]) Say a linked pane is drawn at its first link
tony Nov 4, 2026
441d52c
refactor(scripts[mcp-session]) Test the stand-in mapping and its refusal
tony Nov 4, 2026
405f942
feat(scripts[mcp-transcript]) Read tmux's own state after each call
tony Nov 4, 2026
95780ed
chore(site[mcp]) Refresh the protocol snapshots of seven ports to master
tony Nov 4, 2026
cb0dd52
feat(site[mcp]) Draw each worked example from tmux's own state
tony Nov 4, 2026
a66f6cf
fix(scripts[mcp-transcript]) Find escaped and encoded host paths
tony Nov 4, 2026
f40c845
fix(scripts[mcp-tmux-state]) Wait for window names and bound each read
tony Nov 4, 2026
1543c75
fix(scripts[mcp-session]) Record again after a settle timeout
tony Nov 4, 2026
f07d531
fix(scripts[mcp-transcript]) Run the recorder through the shared guards
tony Nov 4, 2026
ce47828
chore(site[mcp]) Record Ruby's worked example with a named window
tony Nov 4, 2026
9aeaa8b
fix(site[mcp]) Say where the preview's panes and output come from
tony Nov 4, 2026
f65abe4
fix(site[mcp]) Keep Ruby's protocol snapshot at its model's pin
tony Nov 4, 2026
6e66997
fix(scripts[mcp-tmux-state]) Read a server that is exiting as gone
tony Nov 5, 2026
35abb26
fix(api-model[ruby]) Read each program's tmux release
tony Nov 5, 2026
873fc3f
fix(site[ruby]) Pin Ruby at libtmux-ruby 0bf78344f
tony Nov 5, 2026
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
52 changes: 37 additions & 15 deletions packages/api-model/src/languages/ruby-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,38 @@ interface RubyProgram {
interface RubyExamples {
manifest?: { programs?: RubyProgram[] }
files?: { path: string; content: string }[]
/** The tmux release each program's page states, by path. Only an export of manifest version 2 carries it. */
tmux_releases?: unknown
}

/** The release every page stated before the manifest declared one. */
const LEGACY_TMUX_RELEASE = '3.3a'

/**
* Programs the port's manifest selects whose pages must not show them yet,
* by path, each with why it is held and what brings it back.
* Programs an export of an earlier Ruby release selects whose pages must not
* show them, by path, each with why it is held.
*
* A page that tells readers to save a program that fails, or states a tmux
* floor the library refuses, is wrong text. A held program attaches to none
* of its symbols. An entry the manifest no longer lists fails the build, so
* remove it when the pinned revision carries the fix it names.
* floor the library refuses, is wrong text. Those releases export a version 1
* manifest: a program loads a file no page shows, or needs tmux 3.7 while the
* page states 3.3a, and the export names no release to state instead. A held
* program attaches to none of its symbols.
*
* A hold applies only to an export that carries no `tmux_releases`. A release
* that does carries programs that run saved alone and states each program's
* release, so nothing in it is held. Remove the list once the site builds no
* Ruby release older than that.
*/
export const HELD_RUBY_PROGRAMS: Readonly<Record<string, string>> = {
'examples/api/workspace_apply.rb':
'Saved alone in an empty directory it stops with Workspace::ConfigError: it loads ../workspace.yaml, '
+ 'which no page shows. Returns once libtmux-ruby writes the document inside the program.',
+ 'which no page shows. A release with tmux_releases writes the document inside the program.',
'examples/api/control.rb':
'The intro states tmux 3.3a or newer, but the library refuses a control client below tmux 3.7 (capability '
+ 'control-mode). The manifest has no field for a program\'s tmux floor and the model states 3.7 only in '
+ 'prose, so no floor is read. Returns once the manifest declares it and this generator uses it.',
+ 'control-mode), and a manifest of version 1 declares no floor. A release with tmux_releases names it.',
'examples/api/async_control.rb':
'The intro states tmux 3.3a or newer, but the library refuses a control client below tmux 3.7 (capability '
+ 'control-mode). The manifest has no field for a program\'s tmux floor and the model states 3.7 only in '
+ 'prose, so no floor is read. Returns once the manifest declares it and this generator uses it.',
+ 'control-mode), and a manifest of version 1 declares no floor. A release with tmux_releases names it.',
}

/**
Expand All @@ -43,8 +52,11 @@ export const HELD_RUBY_PROGRAMS: Readonly<Record<string, string>> = {
* names a published release, and Bundler needs it spelled out while the gems
* are prereleases.
*
* `held` names programs to leave off their pages, each with a reason; one the
* manifest does not select is an error, so a hold cannot outlive its program.
* Each page states the tmux release the export gives its program in
* `tmux_releases`. An export without it (a release before manifest version 2)
* states 3.3a and applies `held`: programs to leave off their pages, each with
* a reason. A hold the manifest does not select is an error, so a hold cannot
* outlive its program. An export with `tmux_releases` ignores `held`.
*/
export function attachCompleteRubyExamples(
symbols: ApiSymbol[],
Expand All @@ -58,11 +70,16 @@ export function attachCompleteRubyExamples(
if (programs === undefined) return
if (!Array.isArray(programs)) throw new Error('Invalid Ruby example programs')
const listed = programs.filter((program) => Object.hasOwn(program, 'api'))
for (const [path, reason] of Object.entries(held)) {
const releases = bundle?.tmux_releases
if (releases !== undefined && (typeof releases !== 'object' || releases === null || Array.isArray(releases))) {
throw new Error('Invalid Ruby example tmux releases')
}
const holds = releases === undefined ? held : {}
for (const [path, reason] of Object.entries(holds)) {
if (!listed.some((program) => program.path === path)) throw new Error(`Held Ruby example is not in the manifest: ${path}`)
if (!reason.trim()) throw new Error(`Held Ruby example has no reason: ${path}`)
}
const selected = listed.filter((program) => !Object.hasOwn(held, program.path))
const selected = listed.filter((program) => !Object.hasOwn(holds, program.path))
if (!selected.length) return
if (!/^[0-9a-f]{40}$/.test(source.revision)) throw new Error('Ruby examples require a full source revision')
if (!/^[\w][\w.-]*\/[\w][\w.-]*$/.test(source.repository)) throw new Error('Invalid Ruby example repository')
Expand Down Expand Up @@ -90,11 +107,16 @@ export function attachCompleteRubyExamples(
if (!version || !/^\d+\.\d+\.\d+(?:\.[0-9A-Za-z]+)*$/.test(version)) {
throw new Error(`Ruby example gem has no release version: ${program.gem}`)
}
const release = releases === undefined ? LEGACY_TMUX_RELEASE : (releases as Record<string, unknown>)[program.path]
if (release === undefined) throw new Error(`Ruby example has no tmux release: ${program.path}`)
if (typeof release !== 'string' || !/^\d+\.\d+[a-z]?$/.test(release)) {
throw new Error(`Invalid Ruby example tmux release: ${program.path}`)
}
const filename = basename(program.path)
const blocks: NonNullable<DocBlock['examples']> = [
{
lang: 'ruby',
intro: `${api.description.replace(/\n/g, ' ')} Use Ruby 3.3 or newer with Bundler, and tmux 3.3a or newer, on Unix with \`/bin/cat\` and \`/bin/sh\` available. In an empty directory, save this \`Gemfile\`:`,
intro: `${api.description.replace(/\n/g, ' ')} Use Ruby 3.3 or newer with Bundler, and tmux ${release} or newer, on Unix with \`/bin/cat\` and \`/bin/sh\` available. In an empty directory, save this \`Gemfile\`:`,
code: `source "https://rubygems.org"\n\ngem "${program.gem}", "${version}"\n`,
},
{
Expand Down
3 changes: 2 additions & 1 deletion packages/api-model/src/languages/ruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ function source(source: RubySource, artifact: RubyArtifact) {
}

/**
* The model of a Ruby export. `held` names the complete programs left off their pages (see
* The model of a Ruby export. `held` names the complete programs left off their pages when the
* export states no tmux release per program, as exports of earlier releases do (see
* `HELD_RUBY_PROGRAMS`); an export built for a test passes its own, or none.
*/
export function extractRuby(input: unknown, expectedRevision?: string, held: Readonly<Record<string, string>> = HELD_RUBY_PROGRAMS): ApiModel {
Expand Down
42 changes: 42 additions & 0 deletions packages/api-model/test/native-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,48 @@ describe('Ruby native documentation adapter', () => {
expect(model.symbols.find((entry) => entry.id === 'LibTmux::Pane.active?')?.doc?.examples)
.toBeUndefined()
})

it('shows a program held for earlier exports, stating its tmux release', () => {
const input = {
...artifact,
examples: {
manifest: { programs: [{
id: 'control', path: 'examples/api/control.rb', gem: 'libtmux',
api: { symbols: ['LibTmux::Pane#active?'], description: 'Attach a control client.', output: 'ready\n' },
}] },
files: [{ path: 'examples/api/control.rb', content: 'require "libtmux"\n' }],
tmux_releases: { 'examples/api/control.rb': '3.7' },
},
}
const symbol = extractRuby(input, REVISION).symbols.find((entry) => entry.id === 'LibTmux::Pane#active?')
expect(symbol?.doc?.examples?.[0]?.intro).toContain('and tmux 3.7 or newer,')
})

it('keeps the default holds for an export without tmux releases', () => {
// Exports of earlier Ruby releases list the three programs but cannot say
// which tmux they need, so their pages must not show them.
const program = (id: string, path: string, symbol: string) => ({
id, path, gem: 'libtmux',
api: { symbols: [symbol], description: 'A held program.', output: 'ready\n' },
})
const programs = [
program('workspace_apply', 'examples/api/workspace_apply.rb', 'LibTmux::Pane'),
program('control', 'examples/api/control.rb', 'LibTmux::Pane#active?'),
program('async_control', 'examples/api/async_control.rb', 'LibTmux::Pane.active?'),
]
const input = {
...artifact,
examples: {
manifest: { programs },
files: programs.map((entry) => ({ path: entry.path, content: 'require "libtmux"\n' })),
},
}
const model = extractRuby(input, REVISION)
for (const entry of programs) {
const symbol = model.symbols.find((item) => item.id === entry.api.symbols[0])
expect(symbol?.doc?.examples, entry.path).toBeUndefined()
}
})
})

describe('Lua native documentation adapter', () => {
Expand Down
95 changes: 95 additions & 0 deletions packages/api-model/test/ruby-examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,101 @@ describe('complete native Ruby examples', () => {
}
})

describe('the tmux release a page states', () => {
const current = (release: unknown, path = file) => ({ ...bundle(), tmux_releases: { [path]: release } })
const stated = (input: unknown) => {
const target = symbol()
attachCompleteRubyExamples([target], input, source, packages)
return target.doc!.examples![1].intro!
}

it('is the release the bundle gives the program', () => {
for (const release of ['3.3a', '3.7', '3.7c', '3.10']) {
expect(stated(current(release))).toContain(`and tmux ${release} or newer, on Unix`)
}
expect(stated(current('3.7'))).not.toContain('3.3a')
})

it('is 3.3a for a bundle that carries no releases, an export of an earlier Ruby release', () => {
expect(stated(bundle())).toContain('and tmux 3.3a or newer, on Unix')
})

it.each([
['an empty release', ''],
['a branch', 'master'],
['a patch release', '3.7.1'],
['a prerelease', 'next-3.8'],
['a padded release', ' 3.7'],
['a number', 3.7],
['null', null],
])('rejects %s', (_name, release) => {
expect(() => stated(current(release))).toThrow(/tmux release/)
})

it('rejects a program the releases leave out, and releases that are not a map', () => {
expect(() => stated({ ...bundle(), tmux_releases: {} })).toThrow(/no tmux release: examples\/api\/server\.rb/)
expect(() => stated(current('3.7', 'examples/api/other.rb'))).toThrow(/no tmux release: examples\/api\/server\.rb/)
for (const releases of [[], null, '3.7']) {
expect(() => stated({ ...bundle(), tmux_releases: releases })).toThrow(/Invalid Ruby example tmux releases/)
}
})
})

describe('the programs held for an export of an earlier release', () => {
const [applyPath, controlPath, asyncPath] = ['workspace_apply', 'control', 'async_control'].map((name) => `examples/api/${name}.rb`)
const names = [applyPath, controlPath, asyncPath, file]
const gems = ['libtmux-workspace', 'libtmux', 'libtmux-async', 'libtmux']
const ids = ['LibTmux::Workspace.load', 'LibTmux::Server#open_control', 'LibTmux::Async::Server#open_control', 'LibTmux::Server.new']
const everyVersion = [...packages, { name: 'libtmux-workspace', version: '0.1.0.alpha.3' }]
const targets = () => ids.map((id, index): ApiSymbol => ({ ...symbol(), id, publicId: id, name: id, package: gems[index] }))
const input = (releases?: Record<string, string>) => ({
manifest: {
programs: names.map((path, index) => ({
path, gem: gems[index], api: { ...program().api, symbols: [ids[index]] },
})),
},
files: names.map((path) => ({ path, content: code })),
...(releases ? { tmux_releases: releases } : {}),
})
const attach = (releases?: Record<string, string>) => {
const symbols = targets()
attachCompleteRubyExamples(symbols, input(releases), source, everyVersion, HELD_RUBY_PROGRAMS)
return symbols
}
const intros = (symbols: ApiSymbol[]) => symbols.map((entry) => entry.doc!.examples!.length > 1 ? entry.doc!.examples![1].intro : undefined)

it('names the three programs whose pages mislead readers', () => {
expect(Object.keys(HELD_RUBY_PROGRAMS).sort()).toEqual([applyPath, controlPath, asyncPath].sort())
})

it('stay off their pages for a bundle with no releases, while the program beside them states 3.3a', () => {
const symbols = attach()
expect(symbols.map((entry) => entry.doc!.examples!.length)).toEqual([1, 1, 1, 5])
expect(intros(symbols).at(-1)).toContain('and tmux 3.3a or newer, on Unix')
})

it('appear on their pages for a bundle that carries releases, each stating its own', () => {
const symbols = attach({ [applyPath]: '3.3a', [controlPath]: '3.7', [asyncPath]: '3.7', [file]: '3.3a' })
expect(symbols.map((entry) => entry.doc!.examples!.length)).toEqual([5, 5, 5, 5])
const [apply, control, async, other] = intros(symbols)
expect(control).toContain('and tmux 3.7 or newer, on Unix')
expect(async).toContain('and tmux 3.7 or newer, on Unix')
expect(apply).toContain('and tmux 3.3a or newer, on Unix')
expect(other).toContain('and tmux 3.3a or newer, on Unix')
})

it('are not required in the manifest of a bundle that carries releases', () => {
const releases = { [file]: '3.3a' }
const only = { manifest: { programs: [program()] }, files: [{ path: file, content: code }], tmux_releases: releases }
const target = symbol()
expect(() => attachCompleteRubyExamples([target], only, source, packages, HELD_RUBY_PROGRAMS)).not.toThrow()
expect(target.doc!.examples).toHaveLength(5)
const legacy = { manifest: only.manifest, files: only.files }
expect(() => attachCompleteRubyExamples([symbol()], legacy, source, packages, HELD_RUBY_PROGRAMS))
.toThrow(/Held Ruby example is not in the manifest: examples\/api\/workspace_apply\.rb/)
})
})

it('rejects mutable revisions and invalid repository citations', () => {
expect(() => attachCompleteRubyExamples([symbol()], bundle(), { ...source, revision: 'master' }, packages))
.toThrow(/full source revision/)
Expand Down
Loading
Loading