Keep changes focused, explain the user benefit, and keep the code testable.
You need macOS 26 or later, Apple silicon, and Xcode 27.0 or later.
CI uses macOS 27, Xcode 27.0, and SwiftLint 0.65.1. Use these versions to
reproduce CI locally. Package resolution uses the committed Package.resolved.
git clone https://github.com/kavoye/linen-browser.git
cd linen-browser
xcodebuild -downloadComponent MetalToolchain
xcodebuild test \
-project Linen.xcodeproj \
-scheme Linen \
-destination 'platform=macOS,arch=arm64' \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY=- \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_ENTITLEMENTS=The command removes the entitlements. The keychain access group and the passkey entitlement both need a provisioning profile. CI runs the same command. To build the app in Xcode, set Team in Signing & Capabilities to your own Apple developer team first. See Building.
The project uses Swift 6, Swift Testing, and Main Actor isolation by default. Do not weaken concurrency checks to make a change compile.
- Separate UI, state, persistence, and external-service code.
- Give each type one main responsibility. Split a file when it contains independent features or changes for unrelated reasons.
- Make each distinct SwiftUI section a separate
Viewwith narrow inputs. Computedsome Viewproperties do not create invalidation boundaries. - Prefer
@Observablemodels. Keep view-local@Stateprivate. - Use the existing design primitives in
Linen/Interface/DesignSystemandLinen/Settings/Componentsbefore adding another style. - Keep user-facing strings localizable. Prefer
LocalizedStringResourcein models and string literals in SwiftUI controls.
ASD-STE100 is not a project requirement. It is intended for controlled technical procedures, while this repository also contains product copy and framework terminology. Use the parts that help: common words, active voice, short sentences, and one idea per sentence.
Add a comment when it explains:
- an invariant or a non-obvious reason;
- a security, privacy, concurrency, or performance constraint;
- a framework limitation or private API fallback;
- an interoperability format that the code must match.
Do not narrate the code, preserve edit history, or justify a design by saying that another product does the same thing. Product names belong only where they identify a real format, service, import source, or compatibility contract.
- Add tests for new behavior and for every fixed regression.
- Assert observable outcomes, not merely that code executed.
- Cover success, failure, boundary, cancellation, and persistence paths where they apply.
- Prefer deterministic fakes and injected dependencies to sleeps or live
network calls. Wait for a condition with
waitUntil, which returns as soon as it holds. Fixed sleeps delay every run and can still fail on a slow machine. - Do not assert on a timer you cannot control. Inject the clock or the interval
instead, as
DownloadFlightsand the extension update sweep do. - Protect process-wide dependencies, such as static stubs and shared stores,
with an exclusive-access trait such as
.exclusiveExternalApp. Serializing one suite does not prevent other suites from accessing shared state. - Do not hide a persistent failure with
withKnownIssue. Either make the test deterministic or keep the unsupported check out of the automated suite.
Run the full suite before opening a pull request. CI runs
Tools/check-format.sh, which fails on SwiftLint violations (brew install swiftlint to run it locally). The configuration is .swiftlint.yml. Put a
switch case’s body on the line after the label. Do not write a declaration or
control-flow body inside single-line braces. Short closures, guard … else { return } and accessor lists ({ get set }) stay inline. CI also checks the
blank-tab, tab-switching, command-palette, Start Page and Ask surface budgets in
Tools/check-performance.sh. Each check compares the fastest iteration with a
budget of about twice the measured baseline. A check that is over budget gives
a warning and does not fail the run. These checks detect large regressions
rather than small percentage changes. Change a budget only with measurements
that justify it.
A case that builds a live WebKit view takes .boundedWebViews. The trait takes
one of a small number of slots, equal to half the machine’s cores. Starting every case
together exhausts WebContent processes and turns resource pressure into
unrelated navigation failures. Put the trait on the tests that build a view, not
on the suite around them, so pure cases do not queue for a resource they never
use. Add .serialized as well when the cases in a suite share state.
Linen.xctestplan runs with per-test timeouts: 120 seconds by default, 300 at
most. A stalled test times out and reports its name without blocking the full run.
Both frameworks run from this plan. A new test needs no entry in it, because the
plan lists the target, not its tests.
Tests get their own support directory, so a test can create profiles, write permissions or fill the download list without reaching the files of an installed copy.
- Use the shortest familiar label that is unambiguous.
- Never use forced metaphors, poetic phrasing, or marketing filler in product copy. State the action or result directly: “Import your bookmarks,” “Choose a Tab for Lyrics,” and “Change the model or reasoning level.” Keep necessary instructions, accessibility information, and permission consequences.
- Use Title Case for menu items, window titles, settings page names, and buttons that name a command. Use sentence case for options, captions, and row titles. One command keeps one capitalization on every surface of the same kind.
- Use US English in user-facing strings, a typographic apostrophe, and an em dash. Code comments stay ASCII.
- Name the feature “assistant” in copy the user reads. Use “agent” only for the machinery it runs on: tools, activity, the trace. Use “AI” only in disclosure contexts.
- Do not explain standard controls unless the consequence is unusual.
- Put consequences before implementation details in alerts and permission prompts.
- Support keyboard use, VoiceOver labels, reduced motion, and increased contrast when the surrounding component does.
Stage mode loads sample browsing data for screenshots and recordings.
Set the session in Linen/Core/Stage/StageSet.swift: pinned tabs, folders,
loose tabs, history and downloads.
LINEN_STAGE=1 build/DD/Build/Products/Debug/Linen.app/Contents/MacOS/LinenPrepare the session once. Staged tabs load real websites, which may show cookie banners and region prompts on first use.
- Launch with
LINEN_STAGE=1. - Dismiss every banner on every staged tab.
- Add a model API key in Settings if a recording needs an agent turn.
- Quit. Your choices are saved in the stage data store for the next launch.
A stage run writes to its own support directory, its own website data store and
its own preference domain. It cannot change the real installation’s history,
cookies, tabs or settings. Delete $TMPDIR/linen-stage to reset it, or set
LINEN_STAGE_HOME to keep more than one staged session.
Linen uses Conventional Commits. Start the subject with a type, add a scope in parentheses when the change belongs to one area, then a colon and a summary. Write the summary as an instruction: one imperative sentence, capitalized, with no final period. Keep the whole subject to 72 characters or less.
fix(sidebar): Keep the selection after a drag
feat(downloads): Keep the list after a quit
test(agent): Stop a wedged suite from holding the run
Use one of these types:
feat: a new capability a person can use.fix: a correction to behavior.perf: a change that makes existing behavior faster.refactor: a change that keeps behavior the same.test: a change to tests only.docs: a change to documentation only.build: a change to the Xcode project or to a package dependency.ci: a change to a workflow in.github/workflows.chore: a change that no other type describes.
Put an exclamation mark after the type for a change that breaks an existing
setup, such as feat!:.
Give the reason for the change in the body. Say what the code did before, and why that was wrong:
ci: Stop re-running tests in the release workflow
CI tests every push to main, and a tag must point at a commit on main, so
the release job ran the same suite a second time.
- The app builds without new warnings.
- The full test suite passes locally.
- New behavior has meaningful tests.
- User-facing strings remain localizable.
- The change uses existing visual and interaction patterns.
- Comments explain constraints, not syntax or product comparisons.
- Each commit subject follows Conventional Commits and reads as an instruction.
Linen is Apache 2.0. Section 5 of the license puts each contribution under the same terms, unless you say otherwise in the pull request. There is no separate agreement to sign.
Your contribution also carries a patent license to everybody who uses Linen. That license covers only patents you own that your own contribution needs. Do not submit code that you cannot license this way.