mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
c5ae6292c464094de80d0191fab50144bff8a027
101
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b2b94dbde7 |
feat(desktop): follow OS language in packaged builds (cherry-pick of #2544 into release/v0.8.0) (#2560)
* feat(desktop): follow OS language in packaged builds
Packaged Electron currently shows Open Design in en-US regardless of
the OS language setting, because the renderer's i18n picks its locale
from `navigator.language` and Chromium hard-codes that to en-US unless
the host process intervenes. Browser users and `tools-dev` users are
unaffected because their `navigator.language` already reflects the
OS / browser preference.
This change:
- Adds `applyOsLocaleSwitch(app)` in `@open-design/desktop/main`. It
reads `app.getPreferredSystemLanguages()[0]` and (when called before
Electron's `ready` event) points Chromium's `--lang` flag at it, so
the renderer's `navigator.language` follows the OS. Safe to call
more than once: `appendSwitch` is a no-op once `app.isReady()`.
- Calls the helper from both Electron entries: `apps/packaged` before
its own `whenReady`, and `runDesktopMain` for tools-dev parity.
- Forwards the resolved locale through
`BrowserWindow.webPreferences.additionalArguments` as
`--od-os-locale=<bcp-47>`, parsed by the preload and exposed at
`window.__od__.client.osLocale`. The host bridge type
(`OpenDesignHostClient.osLocale`) is extended accordingly.
- Updates `detectInitialLocale` in `apps/web/src/i18n/index.tsx` to
read that field as a new step between the existing localStorage and
navigator fallbacks. Browser/web continues to fall through to
`navigator.languages` unchanged.
The explicit `osLocale` channel exists in addition to `--lang` because
some `app.getPreferredSystemLanguages()` strings (e.g. `zh-Hant-TW`,
`pt-PT`) need to round-trip through `resolveSystemLocale` to land on
the right supported locale, which Chromium's `navigator.language`
cannot do on its own.
* fix(web): route OS locale read through getOpenDesignHost
The first cut of detectInitialLocale read `window.__od__.client.osLocale`
directly, which trips `tests/host-boundary.test.ts` — that guard test
keeps web source from referencing preload globals by name so the
boundary stays single-source. Switch to `getOpenDesignHost()` from
`@open-design/host`, and rewrite the i18n test to install the host via
`installMockOpenDesignHost` instead of poking the global directly.
* fix(tools-pack): unblock mac packaged build on pnpm workspaces
Two independent issues prevented `pnpm tools-pack mac build --to all`
from completing on a clean macOS workspace, both unrelated to the
desktop OS-locale change in this PR but bundled here because verifying
that change end-to-end required the packaged pipeline to actually
finish.
1. `apps/web/.next/standalone/node_modules/.pnpm/node_modules/<pkg>`
contained dangling symlinks left by Next's nft trace (e.g. a
`semver -> ../semver@5.7.2/node_modules/semver` link to a
`.pnpm/<pkg>@<ver>` directory pnpm never created). The downstream
`cp { dereference: true }` aborted the whole packaged pipeline
with ENOENT. Walk every artifact tree before copy and unlink
symlinks whose target doesn't resolve. Targets that *do* resolve
stay untouched.
2. Next 16's standalone build under pnpm workspaces does not hoist
peer-dep packages (react, react-dom, styled-jsx) into
`<standalone>/apps/web/node_modules`. The downstream
`web-standalone-after-pack.cjs` audit then does
`createRequire(server.js).resolve('react/package.json')`, whose
module walk falls out of the standalone tree and aborts the
electron-builder phase. Add a `hoistStandaloneNextPeerDeps` step
for the web standalone artifact only: it locates the
`<pkg>@<version>` (not peer-resolved sibling) directory under
`.pnpm` and symlinks it into `apps/web/node_modules/<pkg>`. The
subsequent `cp { dereference: true }` then writes the real
directory into the cache so the packaged tree stays self-contained.
Verified by `pnpm tools-pack mac build --to all` succeeding end-to-end
(zip + dmg + app), then `pnpm tools-pack mac install` and
`pnpm exec tools-pack mac inspect --expr` reading the desired
`__od__.client.osLocale` from the packaged renderer.
* feat(desktop): fold encodeURIComponent + manual locale source + pet window from #2554
Three defensive improvements lifted from @Eli-tangerine's parallel
implementation on #2554, kept consistent with the OS-locale chain
already on this branch:
- The argv value crossing main → preload is now wrapped with
encodeURIComponent / decodeURIComponent so a locale string with `;`,
`=`, or any other Chromium argv special char round-trips cleanly.
BCP-47 region tags don't carry those today, but the renderer parser
no longer has to assume it.
- `setLocale` now also writes `open-design:locale-source = "manual"`
to localStorage, and `detectInitialLocale` only treats the stored
locale as winning when that marker is present. An untagged value
(left over from a future auto-write path, or a stale install) no
longer pins the app to an old language once the host injects a
fresh OS locale. Today `setLocale` is the only writer so the marker
has no behaviour difference yet — this is a defensive net.
- `createDesktopPetWindow` now receives `osLocale` and forwards the
same `additionalArguments` as the main `BrowserWindow`, so the
pet renderer's `__od__.client.osLocale` is consistent with the main
window's instead of being silently undefined.
Co-authored idea credit: changes mirror the locale-piece of
@Eli-tangerine on #2554 — that PR is closing in favour of this one.
Tests: detect-initial-locale gets a new "untagged localStorage value
loses to host locale" case. desktop 62/62, host 13/13, web i18n +
host-boundary 15/15 stay green.
* feat(web): fold onboarding view styles from #2554
Pulls the 747-line addition to `apps/web/src/styles/home/entry-layout.css`
from @Eli-tangerine's #2554 — the visual layer for the global onboarding
flow (`/onboarding` view, Connect / About-you / Design-system steps).
The view itself was already plumbed through `EntryShell.tsx`; this adds
the styling that makes it shippable on v0.8.0.
#2554 is closing in favour of this branch, so the CSS lands here so the
onboarding work doesn't get dropped on the floor.
Co-authored idea credit: @Eli-tangerine — original styling on #2554.
* fix(tools-pack): make hoistStandaloneNextPeerDeps idempotent across builds
Addresses non-blocking review by @PerishCode on #2560: the previous
`if (await pathExists(linkPath)) continue;` guard uses `access()`,
which follows symlinks. A stale symlink from a previous build whose
`.pnpm/<pkg>@<version>` target moved (e.g. after a react/react-dom
version bump that invalidates the workspace-build cache key and forces
a re-run) reports as missing through `pathExists`, then `symlink()`
rejects with EEXIST and the unhandled rejection aborts the packaged
build.
Switch to `lstat` (which does not follow the link) so we can tell
"genuinely empty slot", "real directory left by Next" and "stale
symlink" apart, then unlink stale entries before re-creating. Also
move `stripBrokenSymlinks` ahead of `hoistStandaloneNextPeerDeps` in
`copyWorkspaceBuildArtifactsToCache` so any leftover dangling links
that survived a previous run are cleared before hoist tries to write.
|
||
|
|
526c7f7c26 |
Fix packaged auto-update release validation (#2565)
* fix: tighten packaged updater flow * test: prune noisy extended ui coverage * fix: hide unpublished release artifacts * test: validate release updater channels * fix: align prerelease release namespaces |
||
|
|
ebf4a3ffca |
feat(release): upload browser sourcemaps to PostHog for packaged builds (#2508)
* i18n: add translations for media provider coming soon section (#2415) * i18n: add translations for media provider coming soon section - Add 'settings.mediaProviderComingSoonHint' key to all 19 locales - Replace hardcoded English strings in SettingsDialog.tsx with i18n keys - Reuse existing 'tasks.comingSoon' and 'settings.agentInstall.docs' keys - Resolves TODO(i18n) comment at line 5091 * fix: escape single quotes in translation strings * fix: escape all single quotes in English translation string * feat(release): upload browser sourcemaps to PostHog for packaged builds Next.js was emitting minified JS with no browser sourcemaps, so PostHog Error Tracking surfaces frames like fO / fz / s4 / tD instead of real file:line locations. This wires up the full pipeline: - apps/web/next.config.ts: enable productionBrowserSourceMaps so next build emits .js.map alongside each chunk. - tools/pack/src/web-sourcemaps.ts: new helper that runs after next build and before any packaging step copies the web output into the Electron resources. Uses @posthog/cli to inject chunk IDs and upload sourcemaps to PostHog, then ALWAYS strips every .map under .next/static so source never ships inside an installer (saves ~14 MB per packaged image too). - tools/pack/src/{mac/workspace,win/app,linux}.ts: call processWebSourcemaps immediately after the @open-design/web build step. - tools/pack/src/config.ts: read POSTHOG_CLI_API_KEY + POSTHOG_CLI_PROJECT_ID (with POSTHOG_PERSONAL_API_KEY / POSTHOG_PROJECT_ID aliases) and expose them on ToolPackConfig with the same shape as the existing posthogKey / posthogHost fields. - .github/workflows/release-{beta,preview,stable}.yml: pass the new secrets through so all three release channels symbolicate stacks. When the API key is missing (PR builds, forks, local contributor builds), the helper logs and skips the upload — but still strips .map files. The strip step is unconditional because shipping a sourcemap is equivalent to shipping the source. Adds tools/pack/tests/web-sourcemaps.test.ts covering: missing chunks dir silently noop, no-map noop, strip-only path when credentials are absent, recursive walker for nested subdirectories. CLI happy path is left to the release workflow itself. Required follow-up (cannot push from code): add a repo secret named POSTHOG_CLI_API_KEY (the phx_ personal API key) and a repo var named POSTHOG_CLI_PROJECT_ID (the numeric project id, 420348 for our project) in nexu-io/open-design settings before merging. * fix(web-sourcemaps): use management host for CLI, not ingest host POSTHOG_HOST is the ingest URL (us.i.posthog.com) used by the runtime SDK to POST events to /capture/. The @posthog/cli sourcemap upload talks to the **management** API (us.posthog.com) and gets a 404 on the ingest host. The two are not interchangeable. Adds a separate `posthogCliHost` field on ToolPackConfig sourced from POSTHOG_CLI_HOST (with no fallback to POSTHOG_HOST). When the env is unset the @posthog/cli defaults to the US Cloud app host on its own, which is correct for our project — so this PR doesn't need a new repo variable for it. --------- Co-authored-by: Nicholas-Xiong <2482929840@qq.com> |
||
|
|
33b20aa9cb |
Fix Windows reinstall detection and duplicate plugin nav (#2466)
* Fix Windows silent reinstall detection * Remove duplicate entry plugins nav |
||
|
|
899c9fe4d8 | Support nightly and preview package identity (#2437) | ||
|
|
15d08d4158 | feat: add windows packaged auto update flow (#2362) | ||
|
|
80d305858b |
feat(diagnostics): add one-click log export from Settings → About (#798)
* feat(diagnostics): add one-click log export from Settings → About
Adds a new "Export diagnostics" entry under the About section that bundles
daemon/web/desktop logs, machine info, and recent macOS crash reports into
a zip the user can share when reporting issues.
- Browser hits a new daemon HTTP endpoint and triggers a download.
- Electron uses an IPC bridge with the native save dialog and reveals the
saved file in Finder/Explorer; the Help menu also exposes it as a
fallback when the daemon is unresponsive.
Packaging + redaction lives in a new @open-design/diagnostics package so
both surfaces share it. Sensitive JSON keys, URL query secrets, and the
current user's home path are redacted before packaging.
* build(nix): include packages/diagnostics in daemon build targets
The Nix daemon derivation builds workspace siblings in dependency order
before compiling apps/daemon. Without @open-design/diagnostics in that
list, the daemon TypeScript build fails inside the Nix sandbox with
`Cannot find module '@open-design/diagnostics'` because pnpm install
only creates the symlink — the dist output that the package.json
exports point at isn't produced until each sibling's build script runs.
* build(tools-pack): include @open-design/diagnostics in packaged INTERNAL_PACKAGES
Without this, packaged win/mac/linux builds fail with `npm error 404` when
the post-build `npm install --omit=dev --no-package-lock` step in the
assembled app tries to resolve `@open-design/diagnostics@0.2.0` from the
public npm registry. The package is workspace-private, so it has to be
tarballed via `pnpm pack` and file:-referenced from the assembled
package.json like every other internal workspace dep that daemon/desktop
depend on.
Also wires the package's `pnpm --filter ... build` into the pre-pack
workspace build step so the dist/ exists before pnpm pack runs, and
updates the two test fixtures (`win-app.test.ts`, `workspace-build.test.ts`)
that mirror INTERNAL_PACKAGES.
The diagnostics package itself is repinned to exact dependency versions
already used elsewhere in the workspace (`jszip 3.10.1`, `@types/node
20.19.39`, `esbuild 0.28.0`, `typescript 5.9.3`, `vitest 4.1.6`) so it
passes the new `pnpm guard` exact-version rule and produces a minimal
lockfile diff vs main (additions only, no resolution-string churn).
* fix(diagnostics): include `~` in bearer-token redaction char class
RFC 6750 token68 syntax allows `~`, so tokens like `Authorization: Bearer
abcd~efgh` were only partially matched by `HTTP_AUTH_SCHEME_RE`. The
regex stopped at the first `~`, leaving the tail (`~efgh`) un-redacted in
the exported diagnostics zip — a clear leak since this feature explicitly
generates support bundles for external sharing.
Add `~` to the character class and a regression test.
* fix(diagnostics): only collect renderer.log from desktop
`buildSidecarLogSources` unconditionally added `logs/${app}/renderer.log`
for daemon/web/desktop, but only the desktop runtime writes a renderer
log (see apps/desktop/src/main/runtime.ts) — daemon and web are pure
Node services with no Electron renderer. Every export therefore produced
missing-file placeholders and manifest warnings for the two phantom
paths, polluting the bundle.
Gate the renderer.log source on APP_KEYS.DESKTOP so the daemon-side
collector matches the desktop-side collector in apps/desktop/src/main/
diagnostics.ts:63.
* fix(diagnostics): mirror desktop-side renderer.log gate
The previous fix only updated the daemon-side `buildSidecarLogSources`
in `apps/daemon/src/diagnostics-export.ts`. The desktop-side collector
at `apps/desktop/src/main/diagnostics.ts` had an identical copy of the
same bug that I overlooked: it also unconditionally added
`logs/${appKey}/renderer.log` for daemon/web/desktop, producing
missing-file placeholders + manifest warnings for the two phantom paths
on every desktop-initiated export.
Apply the same `appKey === APP_KEYS.DESKTOP` gate here so both export
entry points (browser via daemon HTTP, Electron via native save dialog)
emit the same clean manifest.
* feat(diagnostics): add `od diagnostics export` CLI subcommand
AGENTS.md's dual-track capability-exposure contract requires every
user-facing feature to ship on both the web UI and the `od` CLI. The
diagnostics export was only reachable through Settings → About and the
desktop Help menu; this commit closes the loop with an `od diagnostics
export [<path>] [--json]` subcommand registered in SUBCOMMAND_MAP.
The CLI is a thin shell over the existing GET /api/diagnostics/export
endpoint — same zip output, same redaction, same crash-report scope.
Defaults to writing `open-design-diagnostics-<timestamp>.zip` in the
current directory; `--output <path>` or a positional arg overrides.
`--json` prints `{path, sizeBytes}` for shell pipelines.
Use cases this unlocks:
- A CI script can `od diagnostics export ~/artifacts/bundle.zip` after
a failed run.
- Bug reporters on headless boxes can grab a bundle without booting
the web UI.
- `od doctor` follow-ups can collect a full snapshot when a probe fails.
* fix(diagnostics): surface non-sidecar launch in manifest warnings
`buildSidecarLogSources()` returns `[]` when the daemon has no sidecar
runtime context, which is the standard `od` (plain) launch path —
`runDaemonCliStartup()` -> `startDaemonRuntime()` does not pass a
runtime. Settings → About and the new `od diagnostics export` previously
reported success but produced a bundle with only the summary JSONs, so
operators could not tell "no logs because plain launch" from "no logs
because something genuinely broke."
- Extend `DiagnosticsContext` with an optional upstream `warnings:
string[]` that `buildManifest` merges into the manifest warnings.
- Emit STANDALONE_LAUNCH_WARNING from the daemon handler when
`options.runtime == null`. The warning names the limitation and
points the user at the sidecar entry points that DO capture logs.
- Add a regression spec at `apps/daemon/tests/diagnostics-export.test.ts`
that drives the handler with `runtime: null` and asserts the warning
surfaces in `summary/manifest.json` (and that `files` is empty so a
user reading the bundle does not confuse "no log sources" with
"missing files").
|
||
|
|
2c128e0e91 | refactor desktop host bridge (#2246) | ||
|
|
4424f08be0 |
[codex] Add packaged desktop auto-update (#1375)
* Add packaged desktop auto-update * Handle counted beta nightly update versions * Refresh desktop auto-update branch for main * Serialize desktop updater operations * Refresh auto-update branch for packaged paths |
||
|
|
d55f05fcfa |
fix: remove dead ternary in WORKSPACE_ROOT resolution (#487)
* fix: remove dead ternary in WORKSPACE_ROOT resolution
Both tools/dev and tools/pack config files had:
ENTRY_DIR_NAME === "dist" ? "../../.." : "../../.."
with identical branches. Since `src/` and `dist/` are siblings under
`tools/{dev,pack}/`, both resolve to the same path. The ternary and
ENTRY_DIR_NAME constant were dead code — simplify to "../..".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Fix workspace root depth
---------
Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
|
||
|
|
22a3b99a47 |
Merge origin/main into preview/v0.8.0
Sync 49 commits from main. Conflicts resolved:
- .github/workflows/ci.yml: kept v0.8.0 granular per-area gating, added main's
linux specs + release-stable.yml + release-preview.yml triggers
- .github/workflows/release-preview.yml: kept v0.8.0's full workflow over main's placeholder
- apps/web/src/components/AssistantMessage.tsx: combined v0.8.0 file-ops
summary with main's stripTodoToolGroups + suppressAskUserQuestionFallbackText
- apps/web/src/components/ChatPane.tsx: kept both new imports
- apps/web/src/index.css: kept both .msg-plugin-chip and .user-copy-btn blocks
- e2e/ui/*.test.ts: kept v0.8.0 openEntrySettingsDialog helper over main's
inline dialog navigation (UI was redesigned in v0.8.0)
- nix/package-{daemon,web}.nix: kept v0.8.0 pnpmDepsHash; rerun nix build to refresh
|
||
|
|
74637f1cb5 |
Add Linux packaged client parity smoke coverage (#1204)
* docs: plan linux client issue 709 * fix: complete linux headless lifecycle routing * feat: add linux packaged inspect * test: add linux headless packaged smoke * ci: add linux headless packaged smoke * ci: smoke linux AppImage release artifacts * docs: document linux packaged client status * chore: finalize linux client audit remediation * docs: add linux client publication packet * test: harden linux client smoke coverage * ci: preserve linux smoke audit evidence * refactor: consolidate linux e2e helpers Move pathExists and the desktop/web/daemon app-key array out of linux.spec.ts into linux-helpers.ts, where expectPathInside and linuxUserHome already live. Keeps the spec file focused on tests and the helpers file as the canonical home for shared Linux e2e utilities. * fix: move linux e2e helpers to lib * fix: address linux release review blockers * fix: drop npm dependency from containerized linux build writeAssembledApp() previously called runNpmInstall() which executed `npm install` directly. Inside the containerized build path, electronuserland/builder:base strips npm/npx/corepack, so the inner tools-pack build would fail at the assembled-app install step. Route the install through OD_TOOLS_PACK_PNPM_BIN: buildDockerArgs sets the env to the standalone pnpm binary it bootstraps, and the new resolveProductionInstallCommand helper consumes that env to run `<bin> install --prod --no-lockfile --config.node-linker=hoisted`. Host invocations with no env set keep the prior npm behavior. --config.node-linker=hoisted preserves the flat node_modules layout that electron-builder packs the same way as npm-installed trees. New tests cover the resolver branches and assert the docker-arg-to- resolver chain end-to-end so reviewers can see the container's inner build receives the env that switches its install away from npm. * fix: harden linux container bootstrap * fix: validate desktop marker liveness in headless cleanup cleanup --headless previously skipped on any parseable desktop-root.json, trapping recovery when the AppImage had crashed and left a stale marker. Validate the marker the same way stopPackedLinuxApp does: if the PID is not in the live snapshot list, proceed through cleanup instead of skipping. Extract the validation into validateDesktopAppImageMarker so the stop and cleanup paths share one definition of live and owned. Tests cover both branches: a stale marker drives cleanup to remove the runtime/output roots, while a live marker drives cleanup to skip and preserve them. |
||
|
|
883598f556 | Build registry protocol in packaged workspaces | ||
|
|
4f15c33595 | Merge remote-tracking branch 'origin/preview/0.8.0' into preview/v0.8.0 | ||
|
|
9ef4b1c048 | Use channel mac executable identity | ||
|
|
43b1b94c8e | Add preview release channel | ||
|
|
e0c76a09f2 | Fix packaged beta build resources | ||
|
|
cba8bf151d | chore: align namespace lifecycle packaging | ||
|
|
b268bbe169 |
Merge origin/garnet-hemisphere (post-9e196d34) — Use Plugin handoff fix
Brings in 11 new garnet commits, most importantly: - |
||
|
|
2ac5854432 |
feat(plugin-inputs): enhance plugin input handling with file upload support
- Added support for file input fields in the PluginInputsForm, allowing users to upload files with serializable metadata. - Updated the HomeHero component to improve the layout and interaction of input fields, enhancing user experience. - Adjusted CSS styles for better visual representation of input fields and their states. - Modified HomeView to reflect changes in authoring chip IDs for better clarity in plugin actions. - Enhanced tests to cover new file input functionality and ensure correct behavior in various scenarios. This update significantly improves the plugin input handling, enabling users to upload files seamlessly and enhancing the overall interaction model. |
||
|
|
59ed000903 | Fix Windows resource cache for Orbit templates (#1554) | ||
|
|
5172e37217 |
Merge origin/main into release/v0.7.0 to prepare merge-back PR
Resolves 7 conflicts via hybrid strategy: - apps/web/src/components/EntryView.tsx: take main (Discord+X pills are forward feature) - apps/web/src/components/Icon.tsx: take main (switch-case refactor) - apps/web/src/components/NewProjectPanel.tsx: take release (preserve #1514 dropdown UX validated in 0.7.0 acceptance) - apps/web/src/index.css: take main (project-target-platforms / instructions chip styles) - apps/web/tests/components/FileViewer.inspect-empty-hint.test.tsx: accept main's deletion - nix/package-daemon.nix, nix/package-web.nix: take main pnpmDepsHash Non-conflicting hunks from #1519 (AppChromeHeader), #1428 (PostHog analytics call sites), and #1540 (release light background) are preserved via auto-merge. |
||
|
|
61163d6b92 | Optimize Windows packaged prebundle flow (#1389) | ||
|
|
e1bc83a476 |
feat(analytics): PostHog product analytics (P0 events, consent-gated, packaged) (#1428)
* feat(analytics): scaffold PostHog product-analytics integration
- Add @open-design/contracts/analytics subpath with the 17 P0 event
payload types, header constants, and code↔CSV enum mapping helpers.
- Add apps/daemon/src/analytics.ts with env-gated posthog-node client,
request-scoped analytics context reader, and artifact-id anonymizer.
- Expose GET /api/analytics/config so the web bundle never embeds the
PostHog key at build time; daemon owns POSTHOG_KEY / POSTHOG_HOST.
- Add apps/web/src/analytics module (identity + lazy posthog-js client
+ React provider) and mount it under <I18nProvider> in app/layout.
No event wiring yet — that lands in the next commit alongside trigger
points (App.tsx, EntryView, NewProjectPanel, SettingsDialog, FileViewer,
runs.ts).
* feat(analytics): wire app_launch, home_view, home_click, project_create_result
- App.tsx: fire app_launch once after first effect tick. handleCreateProject
now emits project_create_result on both success and failure paths.
- EntryView.tsx: home_view (page) gated on agents loading so
has_available_cli isn't transiently false; home_view (asset_panel) fires
per top-tab change with the right result_count.
- NewProjectPanel.tsx: home_click create_button fires before delegating to
the parent; a fresh request_id is generated here and threaded through
onCreate so the matching project_create_result stitches via $insert_id.
- contracts/analytics: tighten createTabToTracking and topTabToTracking
for the worktree branch's renamed tabs (live-artifact, templates).
* feat(analytics): wire settings_view + 3 settings_click events
- settings_view fires on dialog mount and on every section switch,
carrying the active section (mapped via settingsSectionToTracking
for the 16-section worktree layout), execution_mode, and the
selected CLI provider id when present.
- settings_click execution_mode_tab: setMode now emits before/after
values whenever the user toggles between Local CLI and BYOK.
- settings_click cli_provider_card: agent card onClick reports
cli_provider_id via agentIdToTracking (kiro → other).
- settings_click byok_field: onFocus added to api_key, model select,
and base_url inputs; provider_id widened to include google so the
worktree's Gemini protocol slot type-checks.
* feat(analytics): wire studio_view + studio_click chat, studio_view artifact
- packages/contracts/src/analytics/artifact-id.ts: FNV-1a 64-bit helper
produces a 16-hex anonymized id for (projectId, fileName). Stable
cross-platform so the daemon and the web bundle resolve the same id
without a Web Crypto round-trip; daemon now re-exports it.
- ChatComposer: studio_view chat_panel fires once per project mount,
studio_click chat_composer fires on attachment + send buttons with
estimated user_query_tokens (length/4) and has_attachment.
- FileViewer: studio_view artifact fires once per (project, file) at
the dispatcher level, before any sub-viewer renders, with
artifact_kind derived from the renderer registry / file.kind table.
- Widen TrackingExportFormat to include markdown and cloudflare_pages
so the worktree branch's full share menu can emit verbatim.
* feat(analytics): wire studio_click share_option + artifact_export_result
HtmlViewer's share menu now emits both events per click via a
fireShareExport helper:
- studio_click share_option fires immediately on click with the chosen
export_format and a fresh request_id.
- artifact_export_result fires when the export resolves — success for
sync exporters (html, markdown, template) the moment the call
returns, success/failed for async exporters (pdf, zip, deploy)
via .then/.catch. The same request_id threads both events so
PostHog stitches click → result via $insert_id.
DEPLOY_PROVIDER_OPTIONS maps to the CSV's vercel / cloudflare_pages
slots; markdown is now a first-class export_format value.
Also ignore .env.local so local POSTHOG_KEY / .env-style secrets
don't get committed.
* feat(analytics): emit run_created and run_finished from the daemon
POST /api/runs now reads the analytics context off the
x-od-analytics-* headers the web client sets on every fetch, then:
- Captures run_created with project_id, conversation_id, run_id,
model_id, agent_provider_id (mapped via agentIdToTracking),
skill_id, design_system_id, plus the token_count_source marker.
- Schedules a run_finished capture on runs.wait(run) resolution,
mapping succeeded/canceled/failed to success/cancelled/failed and
reporting total_duration_ms.
Both events use a stable insert_id derived from the same uuid so
PostHog dedupes the daemon-side mirror against any future
web-side capture without double-counting.
Token sub-fields (user_query_tokens/system_prompt_tokens/...) stay
omitted in v1 — the claude-stream parser only exposes input/output
totals today. See tracking-doc-issues.md §3.2.
* feat(analytics): emit settings_cli_test_result + settings_byok_test_result
The original BLOCKING-list assumed these CSV P0 events were not
implementable in this branch because main lacked Test buttons. The
worktree HEAD actually wires `handleTestAgent` and `handleTestProvider`
in SettingsDialog, so both events are now in scope.
- handleTestAgent emits settings_cli_test_result on success and
failure paths with cli_provider_id mapped via agentIdToTracking,
result drawn from result.ok / catch branch, error_code from
result.kind or the thrown error name, and duration_ms timed via
performance.now().
- handleTestProvider emits settings_byok_test_result analogously,
using apiProtocol (anthropic|openai|azure|ollama|google) directly
as provider_id — wider than the CSV's 5-value enum, documented in
tracking-doc-issues.md §2.5.
Contracts: add SettingsCliTestResultProps / SettingsByokTestResultProps
plus matching track* helpers. AnalyticsEventName union now covers all
14 P0 events this branch supports.
* feat(analytics): gate PostHog on the existing telemetry.metrics consent
The integration now reuses the same first-launch privacy banner +
Settings → Privacy toggle that gates Langfuse, so a single user
decision controls both telemetry sinks.
- /api/analytics/config now consults the persisted AppConfigPrefs:
it returns enabled=true only when POSTHOG_KEY is set AND the user
has chosen "Share usage data" (telemetry.metrics === true). The
response also echoes installationId so the web client uses the
same anonymous id Langfuse keys off of — one identity per install,
shared across both sinks.
- Web AnalyticsProvider:
- Bootstrap fetch resolves installationId and threads it through
the x-od-analytics-anonymous-id header on every /api/* fetch,
so daemon-side captures (run_created / run_finished /
project_create_result) land on the same person record.
- Exposes a setConsent(granted) method that calls posthog-js's
opt_in_capturing / opt_out_capturing, wired from App.tsx via a
useEffect watching config.telemetry?.metrics. Toggling Privacy
→ metrics now stops/resumes events immediately, no reload.
- app_launch additionally gates on telemetry.metrics so a freshly-
declined user fires nothing, and a freshly-opted-in user fires on
the next reload.
* feat(packaging): bake POSTHOG_KEY into packaged daemon spawn env
Wires PostHog product analytics through the same Langfuse-style build-
secret pipeline so official Open Design builds ship with the key while
fork builds compile without it (the integration short-circuits cleanly
when POSTHOG_KEY is absent).
tools/pack
- resolveToolPackConfig reads POSTHOG_KEY / POSTHOG_HOST from
process.env at packaging time, validates them (no whitespace in the
key, http(s) URL for host, trailing-slash strip), and stamps them on
ToolPackConfig. Fork builds without the env vars simply omit the
fields; the daemon-side gate keeps things off in that case.
- Mac, Windows, and Linux packaged-config writers each append the two
fields to open-design-config.json next to the existing
telemetryRelayUrl entry.
apps/packaged
- RawPackagedConfig / PackagedConfig surface posthogKey / posthogHost
so the Electron entry and headless entry both forward them to the
daemon sidecar.
- buildPackagedDaemonSpawnEnv emits POSTHOG_KEY / POSTHOG_HOST into
the daemon child env when present. The daemon's existing analytics
module reads these via process.env — no daemon-side changes needed.
- The headless packaged path falls back to process.env for fields the
builder hasn't injected, mirroring how OPEN_DESIGN_TELEMETRY_RELAY_URL
is read there.
CI
- release-beta.yml and release-stable.yml expose POSTHOG_KEY (secret)
and POSTHOG_HOST (var) at workflow-env scope so every packaging job
inherits them. PR / fork builds without these set simply skip the
bake step.
Tests
- tools/pack: config.test.ts covers bake-through, fork-build omission,
whitespace rejection, invalid-URL rejection, and trailing-slash
normalization.
- apps/packaged: sidecars.test.ts covers buildPackagedDaemonSpawnEnv
forwarding the keys when present and omitting them when null.
* feat(analytics): enable PostHog autocapture + perf + exceptions
Flip on the PostHog SDK's automatic diagnostic features so we capture
click paths, page transitions, web vitals, dead clicks, and browser
exceptions without scattering instrumentation through the codebase.
Privacy defense lives in one place — apps/web/src/analytics/scrub.ts —
wired in via posthog-js's `before_send` hook so every outgoing event
passes through the same audit point:
- $autocapture / $rageclick / $dead_click / $copy_autocapture:
strips $el_text and value/placeholder/aria-label attrs from any
input, textarea, password input, or contenteditable element. PostHog
autocapture does not capture input.value by default, but $el_text
on a <textarea> reflects the typed content — that's the prompt
body for us, so it has to be scrubbed every time.
- $pageview / $pageleave: drops query string and fragment from
$current_url / $referrer so any future ?q=… can't leak.
- $exception: rewrites file:// and absolute filesystem paths in
stack frames to app://apps/<repo-relative> so we don't ship the
user's home directory.
- Suppresses $opt_in entirely — duplicate of our explicit
setConsent toggle in App.tsx.
Element-level defense in depth is limited to the single most sensitive
surface: the chat composer textarea gets `ph-no-capture` so PostHog
never even generates an event for clicks inside that subtree. Every
other input relies on scrub.ts — sprinkling the class through every
form would be noisy and easy to forget on new surfaces.
The existing Privacy → "Share usage data" toggle continues to gate
every new feature: posthog-js's opt_out_capturing() halts autocapture,
$pageview, $exception, web vitals, and dead clicks alongside the
explicit capture() calls — one global switch.
11 unit tests pin the scrub rules in apps/web/tests/analytics-scrub.test.ts.
* ci(nix): bump pnpmDepsHash for posthog-js + posthog-node additions
Adding posthog-js to apps/web and posthog-node to apps/daemon changed
pnpm-lock.yaml, which Nix's fixed-output pnpmDeps derivation pins by
sha256. The CI nix flake check failed with:
specified: sha256-KF3Mld72/iau+pJmA7HvnanRx8VLtDP0N624SKrtrrc=
got: sha256-PGFgX4lYyeH2TRAXfUq52A3EOa6bb1gO59hPsXhEk3s=
Copy the new hash into both nix/package-web.nix and
nix/package-daemon.nix per the procedure documented in nix/README.md
§"First-build hash pinning".
* feat(analytics): unify PostHog identity with Langfuse installationId
PostHog's distinct_id is the installationId stamped by /api/analytics/
config; Langfuse already reads the same id off app-config.json to
populate trace.userId. With both sinks keying off the same anonymous
identity, dashboards can correlate user actions (PostHog events) with
LLM runs (Langfuse traces) without re-identifying.
Two gaps closed:
1. applyConsent(false) — clear posthog-js's persisted ph_*_posthog
localStorage entry on opt-out via posthog.reset(). Without this, a
user who opts out, then clicks Delete my data, then re-opts in
would see PostHog stitch their new session to the deleted identity
because bootstrap.distinctID only takes effect on first init.
2. applyIdentity(newInstallationId) — Delete my data rotates the
installationId in app-config; App.tsx now watches config.installationId
and calls posthog.reset() then identify(newId) so the next event
batch is fully decoupled from the deleted one. Idempotent on
same-id re-renders so benign config refreshes don't churn PostHog
identities.
The fetch wrapper's x-od-analytics-anonymous-id header also flips to
the new id on rotation so daemon-side captures (run_created /
run_finished) land on the same person record from the very next API
call, not after a reload.
The end-to-end rotation flow is verified against a live PostHog
project; these unit tests pin the safety guards (no-client paths, null
inputs) since stubbing posthog-js's init-loaded callback chain is
brittle.
* fix(langfuse): require both metrics AND content consent for trace reports
Tightens the Langfuse gate so a user who shares anonymous metrics but
NOT conversation content stops emitting Langfuse traces entirely —
Langfuse is used for turn-quality evals which only make sense with
prompt/output bodies. PostHog (product analytics, content-free) stays
gated on `metrics` alone and is unaffected.
i18n: "Conversation content" → "Conversation and tool content" with
hints expanded to mention tool inputs/outputs so the consent surface
matches what the trace actually carries (en + zh-CN).
Bundled here per PR scope — change originated outside this PostHog
PR but lands cleanly on the same files; gating Langfuse strictly
on `content` makes the dual-sink consent model (PostHog = metrics,
Langfuse = metrics + content) symmetric across both i18n locales and
the daemon-side gate.
* feat(analytics): wire byok_provider_option + fix PR review P1s
Adds the BYOK protocol-chip click event (5-value provider_id mirroring
the apiProtocol Settings UI) and resolves four P1 review threads on
PR #1428.
byok_provider_option:
- New SettingsClickByokProviderOptionProps in contracts (provider_id =
anthropic|openai|azure|google|ollama; maps to CSV's 5 values per
tracking-doc-issues.md §2.5).
- trackSettingsClickByokProviderOption helper in apps/web/src/analytics.
- SettingsDialog hooks it on the protocol-chip onClick alongside the
existing setApiProtocol call; is_selected reflects whether the chip
was already active.
Review fixes:
1. client.ts (Siri-Ray): clear `initPromise` when the resolution is
null so a Privacy → metrics opt-in after a previous decline triggers
a fresh /api/analytics/config fetch. Without this, the disabled
response was cached forever — first-session opt-in needed a reload
to start sending PostHog events.
2. provider.tsx (Siri-Ray): replace `url.includes('/api/')` with a
strict same-origin + /api/ pathname check (shared
`isSameOriginApiCall` helper). Outbound third-party URLs containing
`/api/` (e.g. provider.example.com/api/x) no longer receive our
x-od-analytics-* headers.
3. provider.tsx (codex-connector, lefarcen): gate header injection on
`resolvedAnonId` being non-null. When Privacy → metrics is off,
/api/analytics/config returns enabled=false → resolvedAnonId stays
null → wrapper never installs → daemon can't read consent-bearing
headers → no daemon-side PostHog event. setConsent now also clears
resolvedAnonId on opt-out and re-fetches on opt-in.
4. daemon/analytics.ts (defense in depth): createAnalyticsService now
takes dataDir and capture() re-reads app-config to check
telemetry.metrics inside the fire-and-forget wrapper. Even if a
stale header somehow reaches the daemon after opt-out, the capture
is dropped before posthog-node.capture is called.
* fix(web): place "Share usage data" on the right in privacy consent banner
Swap button order in PrivacyConsentModal and the in-settings ConsentCard
so the affirmative "Share usage data" lands on the right and "Not now"
on the left. Matches the OK-on-the-right pattern users expect for
primary actions.
Both buttons keep equal visual prominence (same .privacy-consent-action
styling) so the swap doesn't change the EDPB equal-prominence stance
called out in the original Langfuse telemetry spec.
* feat(analytics): populate run_finished token totals from claude-stream usage
Daemon's claude-stream parser already emits agent usage events with
input_tokens / output_tokens totals; the run service buffers them in
run.events and Langfuse reads them out the same way. The run_finished
PostHog event was leaving these fields empty.
Scan run.events for the most recent agent usage frame on terminal
transition and emit input_tokens / output_tokens / total_tokens when
present. token_count_source flips to 'provider_usage' only when at
least one count landed; runs without provider-side usage data keep
'unknown'.
Provider does not break the input down into the 7 sub-fields the
tracking doc lists (memory / context / attachment / system_prompt /
…); those stay omitted until a parser change exposes them.
* feat(analytics): estimate user_query_tokens from prompt length
The user_query_tokens field for run_created / run_finished was hardcoded
to 0. We can't tokenize without bundling a model-specific tokenizer, but
the character/4 heuristic is the industry-standard estimate when one
isn't available and is enough for funnel analysis (prompt-length cohorts,
short-vs-long-query conversion rates).
Extracted from req.body via the same telemetryPromptFromRunRequest
pattern the daemon already uses for langfuse-bridge (currentPrompt then
message fallback). Only the integer count goes to PostHog — the prompt
text itself never leaves the daemon.
token_count_source flips appropriately:
- run_created with a prompt: 'estimated' (was 'unknown')
- run_created with no prompt: 'unknown'
- run_finished with provider usage: 'provider_usage' (overrides
baseProps' 'estimated' value)
- run_finished without provider usage: inherits 'estimated' or 'unknown'
from baseProps so input/output absent doesn't mask the estimate.
|
||
|
|
43f7fc536a |
Add Langfuse telemetry relay (#1296)
* Add Langfuse telemetry relay * Configure telemetry worker custom domain * Add telemetry relay health check * Harden telemetry relay config |
||
|
|
b5eb8c1647 |
feat: generic skills + split skills/design-templates + finalize-design API (#955)
* feat: general-purpose skills with @-mention composition and user import
Lift skills from "one mode-bound skill per project" to a generic capability
the user can compose per turn:
- Daemon: scan multiple skill roots (user-skills under runtime data, then
the bundled `skills/`); user-imported skills can shadow built-ins by id.
- New `POST /api/skills/import` and `DELETE /api/skills/:id` endpoints,
with CONFLICT/BAD_REQUEST/NOT_FOUND error codes and built-in delete
protection.
- ChatRequest gains `skillIds: string[]`; the chat run concatenates each
picked skill's body (and merges craftRequires) into the system prompt
for that turn only — the project's persistent `skillId` is untouched.
- Web composer: `@` popover now lists skills alongside project files;
picks render as removable chips above the textarea and ride along with
the request as `skillIds`.
- Settings → Library: import form (name/description/triggers/body),
per-card delete for user skills, "user" origin badge.
* chore(web): drop welcome pet teaser + add ds→prompt-template mapping util
- SettingsDialog: remove the inline pet adoption teaser from the welcome
panel so the first-run modal stays focused on configuration.
- New `inferPromptTemplateCategoriesForDs(ds)` helper that maps a design
system's authored metadata to prompt-template gallery categories.
Imported by the design-system gallery wiring on a sibling branch; no
callers in this branch yet.
* feat: split skills/design-templates and add finalize-design API
Phase 0 of the skills/design-templates refactor (specs/current/
skills-and-design-templates.md):
- Move ~104 rendering catalogue entries from skills/ to design-templates/
and keep skills/ for the small set of functional skills that *do work*
on user input (utilities, briefs, packagers).
- Add design-templates/AGENTS.md and skills/AGENTS.md describing the
contract, and a brand-agnostic craft/ surface for opt-in craft rules.
- Daemon: add DESIGN_TEMPLATES_DIR / USER_DESIGN_TEMPLATES_DIR roots and
an /api/design-templates surface mirroring /api/skills. Asset/example
routes still span both registries so existing srcdoc URLs keep
resolving across the rename.
- Web: split LibrarySection into SkillsSection + DesignSystemsSection,
rename the EntryView "Examples" tab to "Templates", and update locales
+ the New-project picker accordingly.
Adds the finalize-design endpoint:
- New apps/daemon/src/finalize-design.ts and packages/contracts/src/api/
finalize.ts — one-shot synthesis of a project's transcript + active
design system + current artifact into <projectDir>/DESIGN.md via the
Anthropic Messages API. Per-project .finalize.lock mirrors the
transcript-export hygiene from PR #493; provider credentials are not
persisted by the daemon.
Other supporting changes:
- README + AGENTS.md updates to document the new directory split and
craft/ surface, plus i18n strings across 13 locales.
- Test refactors and new coverage (finalize-design, runs, sidecar
server, plus refreshed daemon integration tests).
- .gitignore: scope the *.exe ignore to /OpenDesign.exe so legitimate
vendor binaries are no longer hidden.
* fix(merge): move clinical-case-report to design-templates/
Origin/main added the clinical-case-report skill under skills/ before
the skills/design-templates split landed. Its od.mode is prototype, so
per specs/current/skills-and-design-templates.md it is a design template
and belongs alongside the other rendering catalogue entries — not under
the slimmed-down functional skills/ root. Moving it keeps the EntryView
Templates tab consistent with origin/main's intent.
* feat(skills): curated design/creative catalogue + collapsible Settings rows
Seed ~100 curated design/creative skill stubs under skills/ sourced from
awesome-claude-skills (ComposioHQ) and awesome-agent-skills (VoltAgent).
Each stub carries an od.category tag so the new filter pill row in
Settings -> Skills can group them. The seed script
(scripts/seed-curated-design-skills.ts, pnpm seed:curated-design-skills)
is idempotent: it only creates folders that don't already exist, so
hand-edited stubs are never overwritten.
- Daemon: parse and surface od.category on SkillInfo with a strict slug
normaliser; mirror the field on SkillSummary in @open-design/contracts.
Category is purely a UI hint — system-prompt composition is unchanged.
- Web: rewrite SkillsSection from a left-list / right-detail grid into a
vertical stack of collapsible rows mirroring the External MCP panel
(header always visible with name + mode/source/category pills + per-row
enable toggle; SKILL.md preview, file tree and inline edit form expand
on demand). Add a Category filter row above the list. Reorder Settings
nav so Skills + External MCP sit above the Composio/MCP cluster. Update
composer placeholder/hint across 17 locales to advertise '@ files or
skills · / for commands'.
- Docs: extend skills/AGENTS.md with the curated catalogue rules
(idempotency, category vocabulary, no upstream vendoring).
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(skills): teach localized-content + system-prompt tests about the skills/design-templates split
mrcfps blocking review on PR #955: the skills/design-templates split
(
|
||
|
|
421ddf553c | fix(pack/win): close running app before silent reinstall (#1238) | ||
|
|
cc343f8828 |
ci: optimize beta release packaging cache (#1095)
* ci: optimize beta release packaging cache * fix: version windows builder cache * fix: forward linux app version in container |
||
|
|
dcfab797c2 |
[codex] Add stable nightly promotion gate (#962)
* Upload beta e2e spec reports to R2 * Expose beta report URLs in summary * Complete Indonesian deploy locale keys * chore: factor release workflow scripts * chore: bump packaged beta base version * test: wait for mac packaged runtime health * fix: capture mac packaged startup logs * chore: improve mac release build observability * fix: ad-hoc sign unsigned mac builds * chore: diagnose mac packaged startup * fix: relax unsigned mac launch signing * chore: improve mac launch diagnostics * chore: simplify beta mac release artifacts * fix: align packaged mac smoke launch config * fix: externalize mac daemon wasm dependency * chore: require signed stable mac releases * fix: use stable app version for nightly package builds * chore: clean release artifacts after publish * chore: publish beta reports as zip * ci: disable beta mac tools-pack cache * fix: skip mac framework binary symlinks when signing * fix: sign mac framework version bundles * ci: disable beta mac pnpm cache * chore: align stable release reports * ci: require matching nightly before stable release * ci: avoid mac pnpm cache for packaged smoke |
||
|
|
b06f26a5fd |
test: strengthen e2e PR coverage (#796)
* test: strengthen e2e PR coverage * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.1 (runner=fixer, agent=opencode) * ci: cache Windows packaged smoke builds * test: fake additional agent runtimes * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Route tools-pack mac starts through a launch-time packaged config override so portable packaged smoke runs keep using the namespace runtime root that inspect and logs expect. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Fall back to the packaged app's embedded config when the build output config is missing so installed mac starts still work. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: align packaged mac PR smoke with tools-pack runtime mode Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Keep blake3-wasm out of the packaged mac daemon prebundle so the standalone runtime loads the Cloudflare asset hasher from node_modules instead of crashing in ESM. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix: address e2e PR feedback Skip the portable mac launch override when the bundled packaged config is missing so installed fallback app targets can still boot with packaged defaults. Add a regression test covering the missing-config start path. Generated-By: looper 0.6.2 (runner=fixer, agent=opencode) * fix(pack): remove duplicate mac prebundle dependency key |
||
|
|
aec9428b08 |
Fix desktop preview and packaged app interactions (#879)
* Fix packaged deck navigation interactions * Fix connector auth in packaged app and localized content coverage * Fix Electron connector browser handoff contract |
||
|
|
56bf6ee1b6 |
feat: agent-callable research command and /search (#615)
* feat: pre-generation research (Tavily) for grounded generation
Adds an optional pre-generation research step so the agent can produce
slides / prototypes / decks grounded in real sources instead of guessing.
User flow:
1. Settings -> Tavily Search -> paste API key (or set TAVILY_API_KEY).
2. Click the new Research button in the chat composer.
3. On send, the daemon runs a Tavily search, prepends the findings
as a <research_context> block ahead of the system prompt, and
spawns the agent. Research progress shows up as status pills in
the chat stream; the agent cites sources inline as [1]/[2]/...
Phase 1 surface:
- Single provider (Tavily), single depth ('shallow'), no LLM
synthesis pass (Tavily's `answer` is the summary).
- Composer toggle only; no popover / depth picker yet.
- Reuses the existing `status` SSE agent payload + StatusPill UI
so no new event variants or renderer code are needed.
Layers touched:
- contracts: ResearchOptions / Source / Findings DTOs;
ChatRequest.research; export from index.
- daemon: apps/daemon/src/research/{index,tavily}.ts orchestrator
+ provider; tavily added to MEDIA_PROVIDERS and ENV_KEYS; hook
in startChatRun before prompt assembly.
- web: ChatComposer toggle + ChatSendMeta; threaded through
ChatPane / ProjectView / streamViaDaemon into ChatRequest.
Side fix (required to land the feature, but useful on its own):
contracts internal relative imports lacked the `.js` suffix that
NodeNext module resolution requires. This was already breaking
`pnpm --filter @open-design/daemon typecheck` on main; without the
fix, none of the new research types were visible to the daemon.
All internal contracts imports now carry `.js`.
Spec: specs/current/research-feature.md (phases 2-4 outlined for
follow-up: composer popover, multi-provider, deep recursion, example
skills with research_recommends).
Verified:
- pnpm --filter @open-design/contracts typecheck/test
- pnpm --filter @open-design/daemon typecheck (the chokidar
project-watchers test is a pre-existing flake, unrelated)
- pnpm --filter @open-design/web typecheck
- node scripts/verify-media-models.mjs
* fix(daemon): clamp Tavily max_results to 20
Tavily's /search endpoint requires `max_results` in [0, 20]; sending a
larger value (e.g. when `research.depth: "deep"` resolves to 30) returns
400 and `runResearch` silently falls back to no-research. Clamp at the
provider boundary so Phase 2 depth tiers above 20 still produce results
instead of failing the request.
Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)
* Remove stale research merge leftovers
* Add agent-callable research search
* Fix Indonesian locale typecheck
* Fix research command invocation edge cases
* Harden slash search prompt expansion
* Honor research source caps in command contract
* Require search reports in design files
* Add research data provider settings
* Wire web research provider fallback order
* Update research provider fallback wording
* Revert "Update research provider fallback wording"
This reverts commit
|
||
|
|
294fe94c67 |
fix(pack/win): close detection gaps that let Open Design.exe stay locked at install time (#821) (#823)
The custom NSIS pre-install flow detects and closes running OD processes before extraction, but two gaps let `$INSTDIR\Open Design.exe` stay locked when the installer reaches `MUI_PAGE_INSTFILES`. The user then sees NSIS's native "file in use" Retry/Cancel dialog (not the custom `RunningInstancesCloseFailed` text), which is what kutzki reported. `DetectRunningInstances` and `CloseRunningInstances` previously matched processes only by `Win32_Process.ExecutablePath` under the install root. WMI returns null `ExecutablePath` for processes the caller cannot fully introspect: insufficient access tokens, processes mid-spawn, protected-process states. A child spawned in the millisecond window between the previous OD running and the installer's detection step can hit this and slip past the filter. Both functions now fall back to a CommandLine prefix match against the install root for null- `ExecutablePath` rows, which is OD-specific enough to avoid false positives without relying on a global `Name` match. `CloseRunningInstances` previously called `Stop-Process -Force` and returned without waiting for the OS to actually finalize the process exit. On Windows the file handle GC for an exiting process is async, so a `MUI_PAGE_INSTFILES` overwrite right after the kill can race the handle release and trigger NSIS's native file-in-use prompt even though the kill succeeded. The function now `WaitForExit(5000)` per PID after the force-stop loop, before returning, so the lock has time to clear before NSIS attempts the overwrite. Both changes were endorsed by @lefarcen in the issue thread after they ran their own code review and confirmed the matching diagnosis. The third part of the proposed fix (cross-platform `before-quit` cleanup in the Electron app) is in scope for #422 and not touched here. Local validation: `pnpm guard` clean. `pnpm --filter @open-design/tools-pack typecheck` fails on a pre-existing issue (missing `@electron/rebuild` devDep in tools-pack/src/win/app.ts on current main, reproducible by checking out main directly without my edit), unrelated to this change. The PowerShell embedded in the NSIS template is not exercised by the workspace test suite, so the change has no unit-test surface. Honest caveat: I do not have a Windows packaged-build environment to run `pnpm tools-pack win build --to nsis` and reproduce the locked-file dialog end-to-end. The PowerShell edits are textual and match the patterns already in the file, but a verifying install pass on a real Windows host with a previous OD already installed and running is recommended before merge. Co-authored-by: Nagendhra <nagendhra405@gmail.com> |
||
|
|
cb92c93ae0 |
Migrate beta release publishing to R2 (#805)
* Prebundle standalone web packaged runtime * Harden mac standalone prebundle policy * Prebundle mac daemon packaged runtime * Prune mac Electron locales * Maximize mac release artifact compression * Publish beta mac artifacts to R2 * Use remote R2 uploads for beta releases * Fail fast on beta R2 access issues * Use S3-compatible uploads for beta R2 releases * Decouple beta versioning from GitHub releases * Remove legacy beta metadata source * Address release beta review notes |
||
|
|
6efac8887e |
Improve Windows beta packaging and installer flow (#768)
* Optimize Windows packaged web output * Fix packaged contracts runtime build * Optimize Windows packaged size pruning * Prune Windows root Next payload * Remove Windows bundled Node runtime * Prune Windows standalone duplicate Next * Add tools-pack cache foundation * Cache Windows packaged build layers * Cache Windows workspace builds * Cache Electron-ready Windows app * Split Windows tools-pack module * Cache Windows dir build outputs * Split Windows pack build modules * Document Windows NSIS smoke namespace limits * Move Windows NSIS smoke note to agents guide * Optimize Windows beta packaging * Bump packaged beta base version * Improve Windows installer namespace UX * Improve Windows tools-pack cache keys * Stabilize Windows beta cache version keys * Cache Windows workspace build outputs * Optimize windows release beta cache layers * Cache windows release dependencies * Trim windows release cache before save * Refresh windows tools-pack cache key * Improve windows installer preflight prompts * Fallback NSIS installer strings to English * Fix Windows installer cleanup and preflight * Improve Windows NSIS state logging * Fix system NSIS Persian language alias * Use long-path removal for Windows uninstall * Fix mac tools-pack tests on Windows * Address Windows packaging review feedback * Fix Windows installer cache namespace isolation * Include web output mode in Windows tarball cache key * Use unique Windows release cache save keys |
||
|
|
9b501f12a5 |
Support overriding the Codex executable path (#755)
* Support overriding the Codex executable path * Replace save-as-template prompts with an in-app dialog * Seed local packaged app config from workspace * Fix packaged config and connection test overrides * Keep tools-pack mac config seeding self-contained * Require absolute CODEX_BIN overrides |
||
|
|
4368b8f163 |
feat(linux): add headless mode for install/start/stop operations (#686)
* feat(linux): add headless mode for install/start/stop operations * docs(linux): document headless mode commands and usage * refactor(linux-headless): write web-root.json instead of polling IPC for URL * fix(linux-headless): fail start when web identity never appears instead of returning success * docs(linux-headless): add use-case context and clarify launcher path dependency * fix(linux-headless): ensure launcher, identity and shutdown align with tools-pack - Bake OD_DATA_DIR into launcher so manual runs use the same paths as tools-pack - Validate web-root.json fields before accepting to reject stale identity - Remove web-root.json on successful stop - Add IPC server for graceful STATUS/SHUTDOWN handling * fix(linux-headless): create IPC server before writing web-root.json |
||
|
|
f880fd8c1d |
docs(tools-pack): fix Linux namespace env var (#670)
* docs(tools-pack): fix linux namespace env var * fix(web): remove duplicate Ukrainian prompt labels |
||
|
|
f1cdb2844a |
test(e2e): gate beta packaged runtime (#637)
* test(e2e): gate beta mac packaged runtime * test(e2e): separate ui automation layout * test(e2e): move localized content coverage * chore(release): prepare packaged 0.4.1 beta validation * test(e2e): keep ui lane playwright-only * fix(web): keep chat recoverable after conversation load failure * fix(desktop): honor native mac quit |
||
|
|
14a73d948b |
Fix packaged contracts runtime exports (#577)
* fix packaged contracts runtime exports * fix(packaging): prepare contracts runtime exports on install |
||
|
|
74f1a18b71 | fix(tools-pack): replace corepack with npx in linux container build (#558) | ||
|
|
cc6da191e8 |
fix(version): resolve daemon package.json from any compiled layout (#537)
Settings -> About used to display 0.0.0 in packaged builds because `readCurrentAppVersionInfo` resolved `'../package.json'` relative to `import.meta.url`, which only points at the daemon package root from the flat CLI build (`dist/app-version.js`). The sidecar build emits `dist/src/app-version.js`, where the same relative path lands on the non-existent `dist/package.json`, so `readPackageMetadata` returned null and the version fell back to APP_VERSION_FALLBACK. Walk up from `import.meta.url` to find the nearest real `package.json` instead, so the daemon reports its actual version regardless of whether it runs from TypeScript source (tools-dev), the flat CLI dist, or the nested sidecar dist used by the packaged desktop app. The OD_APP_VERSION env still wins inside `resolveAppVersionInfo`, so callers that already inject it (mac/win packagers) keep working. Also write `appVersion` into the Linux packaged config so Linux follows the same env-injection path as mac/win and stays consistent with the new fallback resolution. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bbdd4e84b5 |
chore: enforce test directory conventions (#496)
* chore: enforce test directory conventions Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only. * ci: use guard and package-scoped tests Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases. * ci: align stable release guard check Use the new repository guard in stable release verification after replacing the residual-JS-only script. * chore: tighten test layout enforcement Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests. * chore: clarify no-emit test tsconfigs Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs. |
||
|
|
3935aeb421 |
Optimize packaged mac artifact size (#424)
* optimize mac package payload reporting * optimize(pack): package standalone web runtime * optimize(pack): default to standalone web runtime * chore(release): bump beta base version * fix(pack): compress mac artifacts and report packaged version * fix(pack): preserve Next server fallback * fix(pack): clarify standalone startup failures * fix(release): gate beta platform builds * fix(web): bind standalone backend to parent * fix(pack): harden standalone and beta publishing |
||
|
|
483e00d24d | fix(pack): bundle prompt templates in desktop resources (#417) | ||
|
|
02638af353 |
Add linux x64 AppImage to tools-pack and release workflows (#369)
* feat(tools-pack): extend config types for linux platform
* feat(tools-pack): add linux resource files (icon, .desktop template)
* feat(tools-pack): export linuxResources paths
* feat(tools-pack): scaffold linux.ts module
* chore(tools-pack): add vitest devdep for linux lane unit tests
* feat(tools-pack): add buildDockerArgs helper for containerized linux builds
* chore: update pnpm lockfile after adding vitest dep
* feat(tools-pack): add renderDesktopTemplate helper
* fix(tools-pack): use @@ICON_PATH@@ token in linux .desktop template
Reviewer flagged the third .replace() in renderDesktopTemplate as dead code
because the template hardcoded Icon=open-design-@@NAMESPACE@@ instead of
using @@ICON_PATH@@. Switch the template to @@ICON_PATH@@ so install logic
controls the icon stem name independent of namespace, and move the
sanitizeNamespace assertion out of the renderDesktopTemplate describe block
into its own describe.
* feat(tools-pack): add matchesAppImageProcess helper
* test(tools-pack): cover matchesAppImageProcess missing APPIMAGE env case
Closes a coverage gap flagged by code review: a process whose executable
matches /tmp/.mount_*/AppRun but has no APPIMAGE env should be rejected.
The implementation already returned false for this case (undefined ===
installPath is false); this test pins the behavior explicitly.
* feat(tools-pack): implement packLinux native build path
* fix(tools-pack): packLinux extra resources, output pre-clear, publish never
Code review flagged three plan-level omissions in packLinux that mac.ts
handles correctly:
1. writeAssembledApp now writes packagedConfigPath (open-design-config.json)
with namespace, nodeCommandRelative, and namespaceBaseRoot. Without it
apps/packaged falls back to defaults at runtime and cannot find the
namespace runtime tree.
2. writeLinuxBuilderConfig now bundles the resource tree and packaged
config into the AppImage via extraResources. Without it the running
app cannot find skills/, design-systems/, craft/, frames/, or the
bundled bin/node.
3. runElectronBuilderLinux now pre-clears appBuilderOutputRoot and passes
--publish never to electron-builder, preventing stale-artifact bleed
between runs and accidental publish attempts in CI when env tokens
are present.
Also aligns appId with mac/win (io.open-design.desktop) and drops a
no-op productNameSafe template-literal.
* feat(tools-pack): implement containerized linux build via Docker
* feat(tools-pack): register linux CLI commands
* fix(tools-pack): align linux electron-builder config with mac.ts
Smoke testing the AppImage revealed the daemon sidecar was missing from
the bundled app.asar:
Cannot find module '@open-design/daemon/dist/sidecar/index.js'
Root cause: writeLinuxBuilderConfig was missing the 'files' field, so
electron-builder used defaults that excluded transitive workspace deps
from the asar. Plus several other mac.ts patterns that I dropped from
the plan: artifactName, executableName, extraMetadata.main/name/
productName/version, npmRebuild=false, nodeGypRebuild=false,
buildDependenciesFromSource=false, compression=maximum, top-level icon.
Switch asar:true → asar:false to match mac.ts (easier to debug missing
files; perf difference negligible for dev installs).
* feat(tools-pack): implement linux install
* feat(tools-pack): implement linux start with extract-and-run
The packaged sidecar's 35-second wait timeout is exceeded when the
AppImage runs from a FUSE-mounted SquashFS (Node module loads + daemon
init are slow through FUSE). Pass --appimage-extract-and-run as the
first arg so AppImage extracts to /tmp first; subsequent file reads
go through a real filesystem and daemon boot completes in time.
Wait for apps/packaged to write desktop-root.json (60s ceiling, generous
to cover AppImage extraction overhead), then fetch desktop status via
sidecar IPC, return the merged LinuxStartResult.
* fix(tools-pack): align linux start helper with mac.ts (log echo + write semantics)
Code review flagged two unjustified divergences from mac.ts in
startPackedLinuxApp:
1. Missing OD_DESKTOP_LOG_ECHO=0 in spawn extraEnv. Without it the
packaged logger echoes to the spawned process's stdout, which goes
nowhere (logFd: null). Added the suppression to match mac.ts.
2. The desktop log truncate writeFile() was wrapped in .catch(() =>
undefined), silently swallowing fs errors that would later surface as
confusing missing-log symptoms. Removed the .catch so errors
propagate per mac.ts.
Also added an inline comment explaining the 60s waitForMarker timeout
(vs mac's tighter ceiling) so the rationale is preserved at the
call site.
* feat(tools-pack): implement linux stop with marker validation
* fix(tools-pack): align linux stop with mac.ts (graceful shutdown + reason strings)
Code review flagged divergences from mac.ts in stopPackedLinuxApp:
1. No graceful IPC SHUTDOWN attempt before SIGTERM/SIGKILL. Mac's
pattern lets Electron renderers + sidecars flush state (SQLite WAL,
logs) first. `gracefulRequested: true` was hardcoded, lying to
callers about what actually happened. Now attempts SHUTDOWN with a
1500ms timeout and reports the actual outcome.
2. The dead-PID-but-marker-exists branch returned reason 'ok' (the
neutral placeholder from readDesktopRootIdentityMarker), which says
nothing useful. Override to 'marker-pid-not-running' to match mac.ts.
3. After a clean stop, remove the desktop-root.json marker so a
subsequent start has a fresh slate (mac.ts does this too).
* fix(tools-pack): clear stale desktop-root.json before linux start
Smoke-testing the install/start/stop loop revealed waitForMarker
returns instantly when a stale marker from a previous run still exists
on disk (e.g., the previous AppImage was killed without going through
'tools-pack linux stop'). The start function then reports success
without actually waiting for the new spawn to write its own marker.
Defensively remove the marker file before spawning. mac.ts removes it
in stop, so a clean stop->start sequence has nothing to remove here.
This only matters for crash-recovery.
* fix(tools-pack): linux stop validates extract-and-run AppImages
Smoke testing exposed a gap from Task 7: matchesAppImageProcess only
recognized FUSE-mode (/tmp/.mount_*/AppRun) but Task 13 launches with
--appimage-extract-and-run, which puts the live executable at
/tmp/appimage_extracted_<hex>/<binary>. Stop's cmdOk validation
returned false, marker validation failed, the running app was
classified as 'unmanaged' and stop refused to kill it.
Fix:
1. matchesAppImageProcess accepts both runner patterns. Extract-and-run
regex matches /^\/tmp\/appimage_extracted_[^/]+\/[^/]+$/.
2. stopPackedLinuxApp now passes paths.installAppImagePath (or the
built fallback) as the canonical install path, not marker.appPath
(which apps/packaged unhelpfully writes as '/' on Linux).
3. linux.test.ts gains 2 new tests covering the extract-and-run mode
(both positive and the wrong-APPIMAGE-env negative case).
* fix(tools-pack): resolve linux paths in stop (typecheck regression from previous commit)
* feat(tools-pack): implement linux logs
* feat(tools-pack): implement linux uninstall
* feat(tools-pack): implement linux cleanup
* docs(tools-pack): document linux lane in READMEs and AGENTS files
* ci(release): add linux x64 AppImage to release-beta and release-stable
Mirrors the existing build_mac/build_win pattern with a build_linux job
in both release workflows. Builds via `tools-pack linux build
--containerized --to appimage` so the AppImage is linked against the
electronuserland/builder glibc 2.27 baseline (portable across distros)
rather than the ubuntu-latest glibc 2.39.
The linux asset is uploaded to the immutable version release tag
alongside mac/win. The beta channel-feed release (latest-mac.yml,
latest.yml) is intentionally not extended with latest-linux.yml because
tools/pack/src/linux.ts has no electron-builder publish block wired,
so the auto-update feed would point users at a feed that never updates.
AppImage auto-update is a separate follow-on.
Linux is unsigned (no signing path in tools-pack yet), so the beta
asset uses the .unsigned suffix matching the windows convention; the
stable asset uses no suffix, matching the stable windows convention.
* fix(tools-pack): propagate --dir/--portable into containerized linux build
The inner `pnpm tools-pack linux build` invocation in `buildDockerArgs`
only forwarded `--to` and `--namespace`. Callers passing `--dir` (e.g.
the new release workflows using `--dir $RUNNER_TEMP/tools-pack`) had
their flag silently dropped: the container defaulted to writing under
/project/.tmp/tools-pack while the host's `findBuiltAppImage` looked at
the caller's chosen `--dir`, producing "expected AppImage not found"
on any non-default tool-pack root. Callers passing `--portable` had
the same drop, baking build-machine runtime roots into shipped artifacts.
Fix:
- Mount `${config.roots.toolPackRoot}:/tools-pack` (new third volume,
alongside the existing /project, /home/builder, and cache mounts).
- Forward `--dir /tools-pack` to the inner build so its output lands
inside the mounted host dir.
- Forward `--portable` when `config.portable` is true.
The mount overlaps harmlessly with /project when toolPackRoot lives
under workspaceRoot (default case): Docker exposes the same host inode
at both paths. The existing .docker-home and .docker-cache/* mounts
continue to shadow the parent at their specific /home/builder paths.
Document the shell-interpolation safety invariant on the inner command:
config.namespace is sanitized at config-time, config.to is enum-validated,
config.portable is boolean -- none can carry shell metacharacters.
Tests: add coverage for the new /tools-pack mount, --dir forwarding,
and --portable propagation (both true and false branches).
Resolves the P1 review feedback from the Codex bot on PR #369.
* docs(tools-pack): polish linux README based on PR review
Addresses non-blocking P2/P3 review feedback on PR #369:
- AppImage launch mode: name the test distros (Ubuntu 24.04, Arch Linux)
and frame the FUSE-vs-extract-and-run gap as an order-of-magnitude
improvement instead of an unspecified slowdown.
- Optional system tools: add a libfuse2 paragraph distinguishing FUSE
launch (needs libfuse2) from extract-and-run (does not), with the
Ubuntu-24-vs-pre-24 package name caveat.
- New section "Format choice: why AppImage first" anchoring the
AppImage-only decision against industry precedent (VS Code, Discord,
Slack, Cursor, Obsidian) so the rationale survives without a reviewer.
- Out of scope: convert the dense one-liner into a bulleted list, mark
AppImage signing as gated on GPG infra + verification flow design
(no ETA), explain the latest-linux.yml gap, and remove the now-stale
"release lane" entry since this PR adds it.
* fix(tools-pack): add --appimage-extract-and-run to installed .desktop launcher
The XDG .desktop file installed by `tools-pack linux install` invoked
the AppImage directly via `Exec=env OD_NAMESPACE=<ns> <exec> %U`. That
bypassed the extract-and-run flag that `tools-pack linux start` applies,
so menu launches and `od://` desktop activations could hit the FUSE
slow path that was already shown to make the daemon sidecar exceed
apps/packaged's 35-second startup timeout. CLI-spawned starts succeeded
while menu-launched starts could fail with the same artifact.
Add `--appimage-extract-and-run` to the template's `Exec=` line and
update the renderDesktopTemplate test expectation. New regression test
locks the flag into place so a future template edit can't silently
drop it.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): treat signal-terminated container builds as failures
`runBuildInContainer` resolved the build promise on `code === null`,
which in Node's child-process `exit` event means the child was
terminated by a signal (SIGTERM, SIGKILL, OOM-killer, parent process
death). A killed Docker build could therefore make `packLinux` report
a containerized build as complete even though the artifact was
partial or missing.
Accept the `signal` argument on the exit handler. Resolve only when
`code === 0 && signal == null`. Otherwise reject with a message
naming either the non-zero code or the terminating signal so the
failure mode is visible in CI logs and `tools-pack linux build --json`
output.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): tear down orphaned process tree on failed linux start
If `startPackedLinuxApp` spawned the AppImage but the post-spawn
readiness path then failed -- either because the 60s waitForMarker
ceiling elapsed without the daemon writing desktop-root.json, or
because fetchDesktopStatus threw -- the detached child was left
running. Because the marker is the only persistent identity source
used by `stopPackedLinuxApp`, future lifecycle commands could not
associate the orphan with the namespace, leaving stale Electron and
sidecar processes plus stale IPC sockets that would interfere with
subsequent starts.
Wrap the readiness wait + status fetch in try/catch. On failure,
collect the spawned child's process tree via listProcessSnapshots +
collectProcessTreePids and stopProcesses() it (the same path
stopPackedLinuxApp uses for its tree teardown), then rethrow the
original error. Cleanup errors are swallowed so the original failure
is preserved in the rejection.
Extract the tree-teardown helper as `teardownOrphanedStart` so the
intent is documented at the call site without inlining 4 imports of
implementation detail.
Resolves a P2 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use `corepack pnpm` in containerized linux build
The inner command in `buildDockerArgs` started with `corepack enable`,
which writes pnpm/yarn/npm shims into the directory containing the
node binary. In `electronuserland/builder:base`, that directory is
owned by root, but the container runs as the host's non-root uid via
`--user` (so build artifacts come out owned by the caller, not root).
The `corepack enable` step therefore fails with EACCES before
`pnpm install` ever runs, blocking the new release `build_linux` job
from publishing the Linux AppImage.
Switch to `corepack pnpm install --frozen-lockfile && corepack pnpm
tools-pack linux build ...`, which resolves and runs the version of
pnpm pinned in package.json's `packageManager` field directly. No
shims, no global mutation, no root writes — corepack just dispatches
to the pinned binary as the unprivileged user.
Update the existing inner-command test to match the new corepack
invocation, and add a regression test that asserts the inner command
contains `corepack pnpm` and never `corepack enable` so a future edit
can't reintroduce the root-write requirement.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): accept menu-launched processes in linux stop/uninstall
stopPackedLinuxApp validated the live process via matchesStampedProcess
against the process command line, requiring a SIDECAR_SOURCES.TOOLS_PACK
stamp. That worked for `tools-pack linux start` (which spawns with
createProcessStampArgs), but rejected menu launches: the installed
.desktop entry only sets OD_NAMESPACE and does not pass stamp args, so
apps/packaged falls back to a SIDECAR_SOURCES.PACKAGED stamp written
into desktop-root.json -- a perfectly valid identity, just not the one
the validator accepted.
Symptoms with the old behavior:
- `tools-pack linux stop` reported `unmanaged` for menu-launched apps
and refused to stop them.
- `tools-pack linux uninstall` would happily remove the AppImage,
.desktop entry, and icon while the packaged app was still running,
breaking handles to the AppImage's mounted/extracted contents.
Switch the validator to read marker.stamp directly (the file content
written by apps/packaged itself, not the process command) and accept
either TOOLS_PACK or PACKAGED. The expected app/mode/namespace/ipc
fields are still required to match. Mirrors the dual-source acceptance
pattern in mac.ts:709-714.
The matchesAppImageProcess (cmdOk) and namespaceRoot checks are
preserved -- the marker still has to point at our AppImage at a path
in our namespace's runtime root.
Drop the now-unused matchesStampedProcess import.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): per-platform --to help text in CLI
addBuildOptions is shared across mac/win/linux but its --to help text
hard-coded the mac targets (all|app|dmg|zip), so:
- tools-pack linux --help advertised --to all|app|dmg|zip even
though resolveToolPackBuildOutput accepts only all|appimage|dir,
sending users at invalid targets and hiding the AppImage option.
- tools-pack win --help had the same problem (advertised mac
targets while accepting all|dir|nsis with default nsis).
Parameterize addBuildOptions(command, platform) and back it with a
TO_HELP_BY_PLATFORM table that mirrors the resolver's accepted targets
in config.ts. Update the three call sites.
Smoke verified by running --help for each platform:
linux: all|appimage|dir (default: all)
mac: all|app|dmg|zip (default: all)
win: all|dir|nsis (default: nsis)
The misleading "--signed: build a signed/notarized mac artifact" line
on win/linux is left alone -- out of scope for this fix and not part
of the review feedback.
Resolves a P3 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): use OD_PACKAGED_NAMESPACE in installed .desktop launcher
The installed .desktop entry's Exec= line set OD_NAMESPACE=<ns>, but
apps/packaged/src/config.ts:9 reads namespace overrides from
OD_PACKAGED_NAMESPACE, not OD_NAMESPACE. The env assignment was a
silent no-op for menu launches: the packaged app fell back to whatever
namespace was baked into open-design-config.json at install time,
ignoring the namespace advertised in the .desktop file.
Practical effect: a .desktop launcher created for namespace "foo"
could end up running as the namespace baked into the AppImage's
shipped config (typically "default"), so installs created across
multiple namespaces could collide silently from menu launches. CLI
launches via `tools-pack linux start` were unaffected because they
pass the namespace through createSidecarLaunchEnv which targets the
correct env var.
Switch the template to OD_PACKAGED_NAMESPACE. Update the existing
renderDesktopTemplate test fixture/expectation, and add a regression
test that asserts the Exec= line uses OD_PACKAGED_NAMESPACE and never
the wrong OD_NAMESPACE name.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
* fix(tools-pack): gate linux uninstall + cleanup on stop status
uninstallPackedLinuxApp called stopPackedLinuxApp first, then deleted
the AppImage / .desktop entry / icon unconditionally. cleanupPackedLinux
Namespace did the same with the output and runtime namespace roots.
Both ignored stop.status -- so when stop returned "partial" (some
processes survived SIGTERM->SIGKILL) or "unmanaged" (the running PID
failed marker validation), uninstall would yank the install files out
from under a still-running packaged app, breaking handles to the
mounted/extracted AppImage contents and leaving an orphan with stale
SQLite WAL files / log handles / IPC sockets.
Extract a small `isSafeToRemoveInstallFiles(stop)` helper that returns
true only for "stopped" or "not-running". Both uninstall and cleanup
short-circuit when it returns false:
- uninstall reports "skipped-process-running" for each removal slot
and "skipped" for the post-install hooks. Existing "ok" / "already-
removed" / "ok"|"missing"|"failed" paths are unchanged.
- cleanup leaves both removed* booleans false and adds a new
`skipped: boolean` field set to true. Old consumers that only read
the booleans see the same "nothing was removed" signal they would
have seen for an already-clean namespace; new consumers can
distinguish "nothing to remove" from "refused to remove."
LinuxUninstallResult.removed.{appImage,desktop,icon} now also accepts
"skipped-process-running"; LinuxUninstallResult.postUninstall.* now
also accepts "skipped". LinuxCleanupResult gains the `skipped` field.
Workspace typecheck clean -- the only consumer is the CLI's printJson,
which doesn't constrain the wire shape.
Resolves a P1 review finding from mrcfps/Looper on PR #369.
|
||
|
|
648374d839 |
fix(platform): wrap cmd.exe shim invocations to survive /s /c quote stripping (#339)
PR #258 standardized agent spawning through `createCommandInvocation`, which on Windows wraps `.cmd` / `.bat` paths in `cmd.exe /d /s /c <line>` and quotes each argument with cmd-style doubled quotes. PR #232's follow-up fix for `shell:true` was lost in that refactor, and the new shape has its own quoting bug on argv-style spawn: 1. cmd.exe `/s /c` strips exactly one leading and one trailing `"` from the rest of the command line. 2. Node, with `windowsVerbatimArguments` unset, escapes each argv element using CommandLineToArgvW rules — so the inner `"path with space"` ends up surfacing to cmd.exe with an extra layer of `\"` escaping that cmd doesn't understand. Together these collapse `"C:\Users\Ethical Byte\...\codex.CMD" --help` into `C:\Users\Ethical Byte\...\codex.CMD --help` with no quoting preserved, and cmd.exe parses the first space as a token boundary — "`Ethical` is not recognized as an internal or external command." See issue #315 for the full repro. The fix mirrors what Node's own `child_process.spawn({ shell: true })` does internally: wrap the entire joined command line in an extra `"…"` and set `windowsVerbatimArguments: true`. The outer wrap absorbs the `/s /c` strip, leaving inner per-arg quoting intact, and the verbatim flag tells Node to pass argv through to CreateProcess unchanged. Changes: - `packages/platform/src/index.ts` - Extend `CommandInvocation` with optional `windowsVerbatimArguments`. - Extract the cmd.exe shim builder into `buildCmdShimInvocation` and apply the outer wrap + verbatim flag in both `createCommandInvocation` and `createPackageManagerInvocation`. - Forward the flag through `spawnBackgroundProcess` and `spawnLoggedProcess`. - `apps/daemon/src/server.ts` — agent spawn forwards `invocation.windowsVerbatimArguments`. This is the call site that hit #315 in the wild (Codex CLI `.CMD` shim, user dir with space). - `tools/pack/src/win.ts` — `runPnpm` and `runNpmInstall` forward the flag through `execFileAsync`. Affects the Windows packaged-build pipeline when run from a path with spaces. - `tools/dev/src/index.ts` — `runLoggedCommand` accepts and forwards the flag; `buildDesktop` propagates it from `createPackageManagerInvocation`. Affects local dev on Windows. Tests: - 9 new unit tests in `packages/platform/src/index.test.ts` stub `process.platform` so both Windows and POSIX branches run on every CI runner. Coverage: - POSIX pass-through. - Windows non-shim binary pass-through. - `.CMD` shim with spaces in the binary path (the #315 repro). - `.bat` shim parity. - Argv elements with spaces alongside the shim path. - Argv elements without whitespace stay unquoted. - `process.env.ComSpec` fallback. - `npm_execpath` short-circuit (cross-platform). - POSIX pnpm pass-through. - Windows pnpm wrapped through cmd.exe. Closes #315. |
||
|
|
0e166bb799 | Optimize Electron release artifact size (#249) | ||
|
|
1edab990bb |
feat(craft): add brand-agnostic craft references + Refero-derived lint rules (#225)
* feat(craft): add brand-agnostic craft references and refero-derived lint rules Introduce `craft/` as a third top-level content axis alongside `skills/` and `design-systems/`, holding universal (brand-agnostic) craft rules that apply on top of any DESIGN.md. Skills opt in via a new `od.craft.requires` front-matter array; the daemon resolves the slug list and injects the matching files between DESIGN.md and the skill body in the system prompt. Initial vendor (MIT, adapted from referodesign/refero_skill): typography craft, color craft, anti-ai-slop. Pilot wired on saas-landing. Extend the existing lint-artifact pass with two refero-derived rules: - P0 ai-default-indigo — solid #6366f1 / #4f46e5 / #4338ca / #8b5cf6 as accent (not just gradients) is the most-reported AI tell. - P1 all-caps-no-tracking — `text-transform: uppercase` rules without ≥0.06em letter-spacing. The craft loader silently drops missing files so a skill can forward-reference future sections (e.g. `motion`) without breaking. * fix(daemon): skip :root token blocks in ai-default-indigo lint The ai-default-indigo P0 check scanned the whole HTML for the raw hex, so brands that intentionally encode indigo as `--accent: #6366f1` in :root and consume it via var(--accent) downstream were flagged as AI-default — a false positive that forced the agent to "fix" valid output. Strip :root token-definition blocks (including attribute-selector theme variants) before scanning, mirroring the existing pattern used by the raw-hex P1 check. Hex still flagged when it appears in component rules or inline styles. * docs(craft): address PR #225 P3 review feedback - craft/README.md: explain why missing craft sections are silently dropped (forward-compatibility) instead of surfacing a warning. - craft/typography.md: ground the 0.06em ALL CAPS tracking floor in Bringhurst-derived typographic practice rather than presenting the threshold as unattributed. - craft/color.md: cover the edge case where a brand's DESIGN.md intentionally encodes indigo as --accent — `var(--accent)` uses remain unflagged because the linter only inspects hardcoded hex. - docs/skills-protocol.md: link the "missing files dropped silently" note back to craft/README.md for the canonical slug list and the rationale behind the choice. * fix(craft): address PR #225 P0 review feedback - tools/pack: copy `craft/` into the packaged resource root alongside `skills`, `design-systems`, and `frames`, so the `od.craft.requires` integration isn't a silent no-op when the daemon resolves `${OD_RESOURCE_ROOT}/craft` in packaged builds. - packages/contracts: add `craftRequires?: string[]` to `SkillSummary` (and therefore `SkillDetail`) so the field that `listSkills()` already returns and `/api/skills(/:id)` already serializes via `...rest` is part of the documented web/daemon contract instead of leaking through as an untyped property. - apps/daemon/lint-artifact: expand the indigo token-strip pass to cover selector lists containing `:root` (e.g. `:root, [data-theme="light"]`) and any rule whose body is custom-property-only (e.g. a `[data-theme="dark"] { --accent: ... }` theme variant). Real component rules with a hardcoded indigo are still preserved so the P0 finding still fires; tests cover the new selector-list and theme-variant cases. * fix(craft): address PR #225 follow-up review feedback - lint-artifact: scope the indigo token-strip to <style> blocks so the rule-shaped regex no longer captures leading `<style>` text into the selector (which broke `:root` recognition for token blocks that mix `color-scheme`/etc. with `--accent`). Run the strip on the extracted CSS instead, with a regression covering `:root { color-scheme: light; --accent: #6366f1 }`. - lint-artifact: tighten the custom-property-only exemption to global theme-scope selectors (`:root`, `html`, `body`, bare attribute selectors like `[data-theme="dark"]`). Component-local rules such as `.cta { --cta-bg: #6366f1 }` are no longer exempted, so an agent cannot launder default indigo through a local var. Regression test added. - craft/anti-ai-slop.md: stop claiming every rule below is enforced by the linter; only several are. The unenforced rules (standard Hero→Features→Pricing→FAQ→CTA flow, decorative blob/wave SVG backgrounds, perfect symmetry) are now flagged inline as "(guidance, not auto-checked)" so the contract with the lint surface stays honest. * fix(daemon): tighten lint-artifact iteration and :root token gating - all-caps-no-tracking: iterate every <style> block. The previous check called `exec` once on a non-global regex, so an artifact whose offending uppercase rule sat in a second <style> block (e.g. a reset block followed by a components block) slipped past. Switch to `matchAll` and break across both loops once a violation is found. Regression test covers a second-block uppercase rule. - ai-default-indigo: stop unconditionally exempting any selector list containing `:root`. The exemption now requires both conditions to hold: every selector in the list is global theme scope AND the body is token-shaped (CSS custom properties or the `color-scheme` keyword). So `:root { background: #6366f1 }` and `:root, .cta { --cta-bg: #6366f1 }` no longer launder a hardcoded indigo through the strip pass. Regression tests cover both bypass shapes. * fix(daemon): scope theme-attr exemption and strip CSS comments in token blocks Address PR #225 review feedback on `ai-default-indigo`: - The bare-attribute branch of `selectorListIsGlobalThemeScope` accepted any `[attr=...]` selector, so a custom-property-only rule on a component/state attribute (e.g. `[data-variant="primary"]`, `[aria-current="page"]`) was treated as a global theme block and stripped before the indigo scan — exactly the component-local indigo laundering this lint is meant to catch. Restrict the exemption to a small allowlist of known theme switches: `data-theme`, `data-color-scheme`, `data-mode`. - `stripTokenBlocksFromCss` split rule bodies on `;` and matched each fragment from the start, so a token block whose body contained a normal CSS comment such as `:root { /* brand accent */ --accent: #6366f1; }` produced a fragment beginning with the comment, failed `isTokenShapedDeclaration`, and the rule was left in scope of the indigo scan — a false P0 on a legitimate token definition. Strip CSS comments before splitting/classifying declarations. Add regression coverage: arbitrary component/state attribute selectors still trip `ai-default-indigo`; `data-color-scheme` theme variants stay exempted; `:root` token blocks with leading, trailing, and between-declaration CSS comments are recognized. * fix(daemon): strip CSS comments and recognize tokens nested in at-rules The all-caps-no-tracking scan ran against raw `<style>` content, so a commented-out rule like `/* .eyebrow { text-transform: uppercase; } */` matched `upperRe` and emitted a P1 for CSS the browser ignores. Strip CSS comments from the style body before structural matching. `stripTokenBlocksFromCss` only matched flat `selector { body }` rules, so a media-query-wrapped token block like `@media (prefers-color-scheme: dark) { :root { --accent: #6366f1 } }` had its outer `@media` rule treated as the selector/body pair and the inner `:root` token block was never stripped, producing a P0 false positive on legitimate responsive theme CSS. Tighten the body alternation to `[^{}]*` so the regex matches innermost rules and recognizes the inner `:root` block directly while preserving the outer at-rule wrapper. * fix(daemon): align ai-default-indigo list with documented cardinal sins The lint's AI_DEFAULT_INDIGO subset omitted #3730a3 and #a855f7, which craft/anti-ai-slop.md lists as P0-blocked solid accents. An artifact could hard-code one of those documented colors as a button fill and slip past the indigo scan unless it happened to be inside a gradient. Bring the lint set to the exact list documented in the craft doc, and tighten the doc's wording from "etc." to an explicit enumeration that points at AI_DEFAULT_INDIGO so the prompt contract and daemon behavior stay in sync. Add regression tests pinning each newly-included hex. * fix(daemon): tighten theme-scope selector and scan inline ALL CAPS The theme-scope exemption used to accept any attribute on `:root`, `html`, or `body` (e.g. `:root[data-variant="primary"]`), letting an agent launder default indigo through a component/state attribute and slip past the `ai-default-indigo` lint. The prefixed branches now require the attribute name to be one of GLOBAL_THEME_ATTRIBUTES, matching the bare-attribute branch. The `all-caps-no-tracking` rule only iterated `<style>` blocks, so inline declarations like `<span style="text-transform: uppercase">` produced no finding even though craft/typography.md treats the ≥0.06em tracking floor as having no exceptions. Added a second scan over `style="..."` attributes that runs the same letter-spacing check and dedupes against the existing `<style>`-block finding so the agent gets a single corrective signal per artifact. * fix(daemon): align uppercase tracking px floor with the 0.06em rule The previous absolute fallback (>=1.5px) was stricter than the craft rule it enforces. `font-size: 12px; letter-spacing: 1px` is 0.083em — above the 0.06em floor — but 1.5px would reject it and trigger an unnecessary correction loop on compliant small-label CSS. Extract `hasAdequateUppercaseTracking`: read `font-size` from the same rule body and compare px tracking against `fontSize * 0.06`; fall back to a conservative >=1px floor when font-size is inherited (covers the default 16px body where 1px ≈ 0.0625em). Apply the helper to both the <style>-block scan and the inline-style scan, and add 12–14px label tests in both branches. * fix(daemon): treat rem letter-spacing as absolute, not per-element em `rem` was previously folded into the same branch as `em` and accepted at the 0.06 threshold. But `rem` is relative to the root font-size (16px default), not the element's own font-size, so on a 48px heading `letter-spacing: 0.06rem` resolves to 0.96px — about 0.02em of the element, well below the 0.06em rule the lint enforces. Convert rem to absolute px through the 16px root assumption and reuse the same px-vs-element-font-size resolution: same-rule `font-size: <n>px` gives an exact `n * 0.06` floor; otherwise the conservative >=1px fallback applies. Add regression tests for 48px headings with 0.06rem tracking (must flag) plus the 16px-element and rem-floor matches that must keep passing, in both <style>-block and inline-style branches. * fix(daemon): resolve var() refs in uppercase tracking lint `hasAdequateUppercaseTracking` only matched literal numeric values, so a tokenized rule like `letter-spacing: var(--caps-tracking)` — exactly the pattern the craft prompt steers artifacts toward — was falsely reported as `all-caps-no-tracking`. Extract `--name: value` declarations from global theme scopes (`:root`, `html`, theme-attribute selectors) once per artifact, then expand simple `var(--name)` (and `var(--name, fallback)`) references in the inspected rule body before applying the existing 0.06em / px-floor / rem-conversion logic. References without a matching token and no fallback stay in place, preserving the conservative "missing tracking" finding. * fix(daemon): resolve rem and var() font-size in uppercase tracking lint Previously the px-vs-element-font-size resolution only matched `font-size: <n>px`. Any rem-based or tokenized display size fell through to the lenient `>= 1px` body-text fallback, so an artifact emitting `.display { font-size: 3rem; text-transform: uppercase; letter-spacing: 1px; }` (a ~48px heading with a 2.88px floor) slipped past the lint that this helper exists to enforce. Resolve `rem` font-size via the same root-font assumption already used for tracking, and treat any explicitly declared but unresolvable unit (`em`, `%`, `calc(...)`, an unresolved `var(...)`) conservatively — refuse the lenient fallback so the rule must use either an `em` letter-spacing or a verifiable px/rem font-size. `var()` font-size declarations resolve through the existing `resolveCssVars` pass before the size scan runs, so the same fix catches the tokenized-display-size pattern (`--display-size: 3rem`). * fix(daemon): parse declarations to ignore custom-prop names in uppercase tracking lint The hasAdequateUppercaseTracking and resolveFontSizePx helpers used substring regexes against the rule body, so a token-name declaration such as `--letter-spacing: 0.08em` or `--display-font-size: 48px` could satisfy the `letter-spacing` / `font-size` checks even though it has no rendered effect — letting actual ALL-CAPS-without-tracking rules slip past the P1 lint. Parse the declaration list, compare exact property names, and skip declarations whose property starts with `--`. Adds regression tests covering token-name letter-spacing (style-block + inline) and a token-name font-size masking the bail-out branch. * fix(daemon): scope indigo token exemption to --accent only Previously stripTokenBlocksFromCss removed every custom-property-only global theme block before the ai-default-indigo scan, which let a laundered indigo token like `:root { --primary: #6366f1 }` consumed via `var(--primary)` slip past the lint. The craft contract is that the only escape hatch is encoding indigo as the design system's `--accent` token; any other token name is still the LLM-default color hidden behind an arbitrary name. Narrow the strip pass so a non-`--accent` token whose value carries an AI-default indigo hex keeps the rule in scope, and add regression tests for `--primary` / `--button-bg` global tokens feeding a CTA, including the at-rule and theme-attribute variants. * fix(daemon): model CSS cascade in tracking lint and detect blue→cyan trust gradients Address PR #225 review feedback (3 comments): - `letter-spacing` / `font-size` selection now picks the LAST matching declaration in the rule body, modeling CSS source-order cascade. `.eyebrow { letter-spacing: 0.08em; letter-spacing: 0.02em }` renders the noncompliant 0.02em the browser actually shows; the previous first-match behaviour silently passed it. - `extractCssTokens` now records every distinct value seen for a token across global theme scopes, and `hasAdequateUppercaseTracking` enumerates each combination so a default-theme value below the floor cannot be rescued by a scoped override that happened to be parsed later (`:root { --caps-tracking: 0.02em }` + `[data-theme="dark"] { --caps-tracking: 0.08em }` now fires). - New `trust-gradient` P0 rule pairs blue/sky tokens against cyan tokens in `linear-gradient(...)` bodies so `blue→cyan` two-stop trust gradients (documented as a cardinal sin in `craft/anti-ai-slop.md`) are actually enforced — both the hex form (`linear-gradient(90deg, #3b82f6, #06b6d4)`) and the keyword form (`linear-gradient(90deg, blue, cyan)`). Adds 11 regression tests covering each path (cascade override in <style> and inline form, font-size cascade shifting the floor, both orderings of the conflicting-token cascade, the don't-over-fire case when every theme value clears the floor, hex / keyword / sky variants of the trust gradient, and the don't-double-fire case when purple-gradient already caught a mixed gradient). * fix(daemon): apply per-scope cascade in extractCssTokens When the same CSS custom property is declared more than once inside a single rule body (e.g. `:root { --caps-tracking: 0.02em; --caps-tracking: 0.08em }`), CSS source-order cascade collapses to the last value; the earlier declaration never reaches any element. `extractCssTokens` was treating intra-scope duplicates as simultaneous theme alternatives, so `hasAdequateUppercaseTracking` enumerated the stale 0.02em and emitted a spurious all-caps-no-tracking finding. Collapse duplicate token declarations within a rule body to the last value before merging into the cross-scope distinct-value map. Cross-scope overrides (separate `:root` and `[data-theme]` rules) remain preserved as distinct values so the conservative theme-cascade check still fires when ANY applicable theme renders below the floor. * fix(daemon): scope tracking lint to innermost rules and per-theme tokens Restrict the upperRe body alternation to [^{}]* so the regex matches innermost CSS rules and skips at-rule wrappers — an outer @media or @supports could otherwise capture as a single rule whose selector was the at-rule and whose body began with the inner selector token, masking the same-rule font-size and letting noncompliant tracking on large headings slip through the lenient inherited-size fallback. Replace the by-name-distinct-values token map with per-scope token records and a buildResolvedThemes pass that materializes one effective map per theme. Paired token declarations now stay paired during evaluation, so theme variants like :root + [data-theme=dark] no longer generate cross-theme cartesian pairings (e.g. default-size + dark-track) that emit false positives on legitimate light/dark themes. --------- Co-authored-by: looper <looper@open-claude.dev> |
||
|
|
f604ff1ec2 | Add Windows beta packaging and release assets (#191) |