Skip to content

feat: upgrade MiniMax default model to M3#417

Closed
octo-patch wants to merge 611 commits into
holaboss-ai:mainfrom
octo-patch:feature/upgrade-minimax-m3
Closed

feat: upgrade MiniMax default model to M3#417
octo-patch wants to merge 611 commits into
holaboss-ai:mainfrom
octo-patch:feature/upgrade-minimax-m3

Conversation

@octo-patch

Copy link
Copy Markdown
Contributor

Summary

Upgrade the desktop minimax_direct (MiniMax) provider configuration to include the new flagship M3 model and use it as the default for the model catalog, AuthPanel defaults, and runtime background-task defaults. The existing MiniMax-M2.7 and MiniMax-M2.7-highspeed entries are retained as available alternatives.

Changes

  • apps/desktop/shared/model-catalog.ts — add MiniMax-M3 entry to the minimax_direct catalog (placed before M2.7 so it is offered first)
  • apps/desktop/src/components/auth/AuthPanel.tsx — add MiniMax-M3 to minimax_direct.defaultModels and switch defaultBackgroundModel to MiniMax-M3
  • runtime/api-server/src/background-task-model.ts — change BACKGROUND_TASK_MODEL_DEFAULTS.minimax_direct from MiniMax-M2.7 to MiniMax-M3
  • website/docs/content/docs/contribute/desktop/model-configuration.mdx — update the documented minimax_direct background-task default

Why

MiniMax-M3 is the new flagship model from MiniMax, with a 512K context window, 128K max output, and image input support across both OpenAI-compatible and Anthropic-compatible interfaces. Making it the default keeps holaOS users on the most capable model when they connect the MiniMax provider, while keeping M2.7 / M2.7-highspeed available for users who prefer the previous defaults.

Validation

  • node --import tsx --test src/background-task-model.test.ts under runtime/api-server/ — 5/5 tests pass
  • TypeScript types unchanged (string defaults only)
  • No API URL or TTS-related configuration touched

jeffreyliimerch and others added 30 commits April 28, 2026 11:54
- refresh the desktop workspace screenshot used by the README
- keep the screenshot update separate from the README link refresh commit
- validation: inspected git diff stats for the updated PNG
- add prominent X and Discord call-to-action badges below the project status badges
- add a centered star prompt to help grow the holaOS community
- validation: not run; README-only change
Removed unnecessary details about prerequisites for Quick Start.
- replace direct pdfjs-dist attachment extraction with unpdf document loading and extraction helpers
- include PDF metadata, links, page text, structured text summaries, embedded image summaries, and rendered page preview metadata in prompt XML
- add @napi-rs/canvas for Node-side PDF page rendering support and remove direct pdfjs-dist dependencies from harness packages
- update PDF attachment prompt coverage for the expanded extraction output
- validation: npm --prefix runtime/harness-host run typecheck
- validation: npm --prefix runtime/harness-host run test
- validation: npm --prefix runtime/harness-host run build
…holaboss-ai#223)

* chore: gitignore website/docs build outputs

The docs-next site (TanStack Start + Fumadocs) leaves .react-router/,
.source/, and build/ in website/docs/ on every build, which keeps
nagging in git status. Match the existing pattern used for the
.vitepress cache/dist directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(integrations): user-global connections + reverse migration + owner_user_id check

Connections are user-global; per-workspace scoping lives entirely in
integration_bindings. The short-lived feat/composio-workspace-scoped-accounts
branch added a workspace_id column to integration_connections that conflicts
with the "one account → many workspaces" requirement.

- state-store: add migrateRevertIntegrationConnectionsWorkspace, which
  detects the legacy column, materializes each non-null workspace_id row
  into a default workspace binding (skipping rows that already have one),
  drops the now-stale workspace-provider index, and ALTER TABLE DROP
  COLUMN workspace_id (SQLite ≥ 3.35).
- state-store: regression tests covering legacy-with-data upgrade
  (3 mixed scenarios) and fresh-DB no-op.
- api-server: add resolveOwnerUserId helper that, when HOLABOSS_USER_ID is
  set (managed/sandbox), rejects bodies whose owner_user_id mismatches.
  In OSS local mode (env unset) behaviour is unchanged.
- api-server: apply ownerCheck to POST /api/v1/integrations/connections
  and /api/v1/integrations/composio/finalize.
- api-server: composio/finalize now accepts an optional workspace_id and,
  when present, atomically upserts a default workspace binding alongside
  the new connection — the connection itself remains user-global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-account Settings → Integrations + per-app binding selector

- Settings → Integrations now lists every active connection per provider
  (no provider-level dedup), each as its own row with its own disconnect
  button. Replaces the old "one Connected pill per provider" UX that
  silently hid additional accounts. The Connect button on a connected
  card now reads "Connect another <provider> account".
- AppSurfacePane's read-only "Connected/Not connected" pill becomes a
  Select dropdown of every active account for the app's expected
  provider. Picking one upserts an app-level integration_binding so this
  workspace's copy of the app uses that account; switching workspaces
  or apps preserves each binding independently. The dropdown's "Connect
  new account" entry opens Settings → Integrations.
- Empty state when the user has no accounts for the provider: a Connect
  CTA that opens Settings → Integrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): tighter Settings → Integrations card + use Composio profile metadata

The previous ConnectedProviderCard was loud — provider header with logo
+ name + "N accounts" subtitle, each account in its own bordered box
with a green status dot, plus a full-width "Connect another" footer
button. Compress to:

- Header line: small (size-5) provider logo + name + an icon-sized
  Connect-another button on the right. Drop the redundant account count.
- Account row: size-3.5 avatar + handle + disconnect icon, no border or
  status dot. The row's mere presence implies "connected".

Account label/avatar now come from Composio profile metadata:
- Renderer fetches composioAccountStatus(account_external_id) for each
  visible connection in parallel after loadData and stores results in a
  Map<connectionId, ComposioAccountStatus>. Append-only across loads so
  transient fetch failures don't blank avatars already on screen.
- Display priority: handle (prefixed @ if missing) → email → displayName
  → account_label (skipping the boilerplate "(Managed)" suffix) →
  account_external_id → connection_id.
- ComposioAccountStatus type extended in both desktop/electron/main.ts
  and desktop/src/types/electron.d.ts with optional handle, displayName,
  avatarUrl, email, and the raw data passthrough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): drop avatars and Composio-id fallback from account rows

Avatars only render reliably for some providers (Twitter has them,
Reddit and LinkedIn often don't), and the empty grey placeholder noise
isn't worth keeping just for the providers that do. Drop the avatar
column entirely.

When metadata is missing the previous fallback chain ended at
account_external_id — but for managed Composio accounts that's the
"ca_xxx" connected_account_id, which tells the user nothing. Skip any
label that starts with "ca_" and fall through to a stable "Account N"
index instead. The metadata fetch still upgrades the label to a real
@handle / email / displayName once Hono /composio/account/:id starts
returning the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): bring back size-3.5 account avatars

Now that Composio whoami delivers reliable avatar URLs across all
managed providers (Gmail, GitHub, Twitter, LinkedIn, Reddit), put the
avatar back on each account row. When the URL hasn't loaded yet (or a
new provider lacks one), fall through to a tiny lettered placeholder
keyed off the display label's first character — matches the avatar's
size-3.5 rounded-full footprint so the row layout stays stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): cascade delete connection bindings + Google avatar referrer

Two bugs from real-world testing of Settings → Integrations:

1. Disconnect button did nothing for any account that was already used
   in a workspace. The runtime's deleteConnection threw 409 ("connection
   is bound to N workspaces — remove bindings first") and the desktop
   set a barely-visible status message. But the user-global model is
   "one account → many workspaces"; deleting an account is the explicit
   way to detach it from all workspaces, not an error condition.
   Cascade-delete every binding (orphan + live) pointing at the
   connection before dropping the row, and report removed_bindings on
   the response so the UI can mention it later if we want a confirm.
   Updated the test that asserted the old 409 path.

2. Google profile avatars (lh3.googleusercontent.com) rendered as broken
   image icons in Electron. The CDN rejects requests whose Referer
   header includes the renderer's localhost dev origin (and even some
   prod origins for hotlink protection). Setting referrerPolicy to
   "no-referrer" on the avatar img drops the header — provider CDNs are
   uniformly happy with no referrer and the image loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): degrade to letter placeholder when avatar URL fails to load

Provider CDNs can be flaky (Twitter rate-limits, LinkedIn auth quirks),
which today shows up as a broken-image icon next to the row. Track
failed-to-load URLs in component-local state and swap to the same
size-3.5 lettered placeholder we already use when avatarUrl is missing
entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): provider logos missing for Gmail/Sheets/Twitter cards

Two stacked bugs in mergeIntegrationCards left integration.logo null
for several connected providers — the screenshot showed grey letter
placeholders instead of the brand mark for Gmail, Google Sheets, and
Twitter / X:

1. TOOLKIT_SLUG_TO_PROVIDER collapses gmail and googlesheets onto
   provider key "google", so the catalog entries with provider_id
   "gmail" / "googlesheets" miss the toolkitByProvider lookup.

2. Hono /composio/toolkits filters out toolkits whose
   composio_managed_auth_schemes is empty unless the user's namespace
   has a custom auth_config for them. Twitter currently has empty
   managed schemes Composio-side, so a user without a custom Twitter
   auth_config never sees the toolkit reach mergeIntegrationCards.

Both root causes are fragile. Sidestep by falling back to Composio's
public logo CDN — `https://logos.composio.dev/api/{slug}` is a stable
public URL keyed by the toolkit slug, no auth required, returns SVG
for every supported provider including Gmail/Sheets/Twitter. Wired in
when toolkit?.logo is null so we still prefer Composio's curated
toolkit logo when present.

Also added the same referrerPolicy="no-referrer" + onError → letter
fallback we use for account avatars, so a logo CDN hiccup degrades
gracefully instead of showing a broken-image icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): UI polish for SettingsDialog + TopTabsBar

- SettingsDialog: arbitrary Tailwind values normalized to scale
  utilities (min-w-[140px] → min-w-35, etc.), Select triggers gain a
  visible border instead of border-transparent, break-words → wrap-
  break-word, plus a few formatter reflows.
- TopTabsBar: user dropdown menu icons match the rest of the app's
  icon-in-menu convention (opacity-60 size-3.5), and useDesktopBilling
  destructure reformatted for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plans): manifest-driven integration routing plan (revised)

Revises the original "yaml-driven" draft after design review:

- Source of truth flips from `app.runtime.yaml` to `marketplace.json`,
  with cross-source consistency enforced by sync endpoint + CI lint
  (closes the silent-drift class this design exists to eliminate).
- D1 / D2 / D3 decisions are surfaced as a "Decisions required BEFORE
  Phase 1" section instead of being scattered through Risks.
- New Phase 0 — Composio byo-creds spike — gates Phase 1 schema. Spike
  script lives in `hola-boss-apps/scripts/composio-spike-byo-creds.ts`
  (separate commit in that repo).
- Phase 2 splits into 2A (helper + dual-source fallback, no entries
  removed) and 2B (per-app cutover with reconciler surfacing badges as
  each app flips). Reconciler ships as no-op code in 2A, becomes
  load-bearing in 2B per-app.
- Phase 3 prerequisites tightened: `<CredentialsModal>` UX wireframe
  added (error display, multi-account, post-connect verify, a11y).
- SDK regen step in Phase 1 explicitly notes both desktop AND
  `frontend/apps/web` consume the regenerated `@holaboss/app-sdk`.
- `_template` integration switched from "comment block in README" to
  "extend `create-hola-app` to emit `marketplace.entry.json` into the
  new app's directory" — comments rot, CLI artifacts don't.
- "No PR to holaOS necessary" claim narrowed to "no PR under the
  existing auth modes" (new auth_mode obviously requires holaOS code).
- File name kept (`yaml-driven-...`) to preserve any external
  references; H1 reads "manifest-driven integration routing".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "docs(plans): manifest-driven integration routing plan (revised)"

This reverts commit f89ad79.

* fix(desktop): unblock Composio connect and unify app icons

The integration connect flow had three layered failures that compounded
into the reviewer-blocking "Connected account not found" loop:

1. composioFetch hard-failed up front when the Better Auth cookie was
   missing/invalid (throwing "Not authenticated"), bypassing the 401
   re-auth retry that requestControlPlaneJson already had. Extracted a
   shared retryAfterSessionAuth helper (single-flight; coalesces parallel
   401s into one sign-in browser window) and routed both call sites
   through it.

2. Node fetch (undici) reused half-closed keep-alive sockets to
   api.imerchstaging.com, surfacing as ECONNRESET right when the polling
   loop hit the staging endpoint hardest. Added fetchWithNetworkRetry
   that catches ECONNRESET / ETIMEDOUT / SocketError on the first attempt
   and retries once after a 200ms backoff with a fresh connection.

3. The polling loop blindly trusted the connected_account_id returned
   by /api/composio/connect — but that id is not queryable via Composio
   v3's /connected_accounts/{id} endpoint until well after OAuth
   completes (and in some cases never resolves). Replaced the by-id poll
   with a snapshot-based approach: capture the user's current connection
   ids before /connect, then poll /api/composio/connections (a stable v3
   endpoint) for any *new* id matching the toolkit. Applied across all
   four call sites (IntegrationsPane, MarketplacePane, FirstWorkspacePane,
   workspaceDesktop). Added a composioListConnections IPC + preload
   binding to support the new flow.

Side fixes bundled because they came up while debugging the same flow:

- composioListToolkits no longer silently returns an empty toolkit list
  on missing cookie (the prior behaviour masked auth issues as "no
  integrations available")
- New AppIcon component centralises the icon resolver as a 4-stage
  fallback: backend manifest URL → Composio CDN → hardcoded SVG kit →
  letter chip. CDN is preferred over local SVGs because its flat brand
  marks render at uniform sizes, eliminating the visual drift between
  cartoon-style Reddit/Octocat-style GitHub and HubSpot-style letter
  placeholder. Adopted in AppCatalogCard and SpaceApplicationsExplorerPane
- Provider display names extended to cover hubspot/attio/calcom/apollo/
  instantly/zoominfo
- Session search dropdown gets 2px gap between rows for breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): track unavailable MCP servers in pi harness

Surface MCP server unavailability through the harness host so the
desktop and api-server tier can render warnings (vs. silently dropping
tools when an MCP backend is down). Adds:

- PiMcpServerUnavailableInfo type carrying { serverId, reason,
  missingToolIds } per failed server
- unavailableServers field on PiMcpToolset and PiSessionHandle so the
  toolset builder can hand a structured summary back to callers
- mcp_server_unavailable event type in both KnownRunnerEventType
  (harness-host contract) and TsRunnerEventType (api-server contract)
  so consumers can listen for these mid-run

No behaviour change for runs where every MCP server starts cleanly —
unavailableServers stays empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): dedupe re-auth flows + install-time account selector

Two bugs reported on PR holaboss-ai#218 once multi-account landed:

  1. Same external account connected twice shows up as two rows. Composio
     re-auth mints a new connected_account_id even for the same real
     account, and the upsert was keyed on connection_id only — every
     re-auth = fresh row. Pre-fix legacy rows had no provider-side
     identity persisted, so they were invisible to any retroactive
     dedupe attempt either.

  2. No way to choose which account to bind when installing an app.
     installAppFromCatalog auto-picked the first active connection for
     the app's expected provider; multi-account users had to install
     first, then go to AppSurfacePane to switch.

Phase 1 — dedupe-on-finalize
- state-store: integration_connections gets account_handle + account_email
  columns (additive migration; legacy DBs heal as users reconnect).
  New findActiveIntegrationConnectionByIdentity finder, scoped per
  (provider, owner), case-insensitive on either field.
- api-server: createConnection looks for an active match by identity
  before creating a fresh row. On hit it preserves connection_id so
  every integration_binding pointing at it stays valid; only volatile
  fields (external_id, secret_ref, label, scopes) refresh.
- desktop main: composioFinalize wrapper does whoami before posting and
  threads handle/email/displayName through. Identity propagation is
  transparent to the four renderer call sites.

Phase 2 — install-time account selector
- AppCatalogCard accepts availableAccounts / selectedConnectionId /
  onSelectAccount. Inline Select renders only when ≥ 2 accounts; one
  account = silent auto-bind; zero = existing "connect first" dialog.
- workspaceDesktop exports APP_TO_PROVIDER_MAP + getProviderForApp;
  installAppFromCatalog accepts an optional connectionId and writes
  integration_bindings (target_type=app, target_id=appId) atomically
  after the app install succeeds.
- AppsGallery groups active connections by provider, tracks per-card
  selection in local state, refreshes after install completes.
- AppSurfacePane integration picker reworked: provider-icon trigger
  with handle as primary line and email / label as secondary
  disambiguation, big enough to read multi-account flows at a glance.

Phase 3 — retroactive merge for users already affected
- Legacy NULL-identity rows were unreachable to the dedupe finder, so
  re-auth still spawned duplicates after Phase 1. Two new escape hatches:
  - composioFinalize backfill: before posting, list (provider, owner)
    rows with NULL identity, whoami each via Composio, PATCH the result
    back so dedupe can match on the next finalize.
  - IntegrationsPane auto-reconcile: after enrichment populates
    accountMetadata for *all* probeable rows, group by (provider,
    resolved-identity); for any group with ≥ 2 rows, keep the oldest,
    backfill identity, call merge to repoint bindings + drop the rest.
    Guarded with reconcileInFlightRef + cancellation flag for unmount.
- New runtime mergeConnections({ keepConnectionId, removeConnectionIds })
  validates (provider, owner) match, repoints every binding inside a
  SQLite transaction (atomic — half-merges can't survive a crash),
  rejects cross-provider/owner merges so unrelated accounts can't
  accidentally absorb each other's bindings.
- New routes: PATCH /connections/:id (now identity-aware) +
  POST /connections/:id/merge.

Misc
- workspaceApps.ts drops the static AppToolDefinition arrays — every
  app's MCP server already advertises tools dynamically; the snapshots
  drifted whenever an app shipped new ones. AppSurfacePane drops the
  "Tools (N)" section that consumed them.

Tests
- state-store: identity columns persist + null when missing,
  findActiveIntegrationConnectionByIdentity (provider/owner scoping,
  case-insensitive, skip inactive, both-keys-supplied), legacy DB
  migration path adds the columns + indexes idempotently. 70/70.
- api-server: dedupe-on-reconnect preserves connection_id and bindings,
  no-identity rows still create fresh, dedupe is provider-scoped,
  identity backfill via updateConnection, merge repoints across
  multiple workspaces, merge rejects cross-provider. 501/501.

Manual smoke (against my local DB with the bug active):
- 2 @jotyy GitHub rows on launch → IntegrationsPane reconcile fires
  after enrichment → merged into 1 row, both prior bindings preserved.
- New re-auth → no duplicate row, existing row's external_id refreshed.
- Marketplace install with 2 active accounts → inline picker, default
  most-recent, binding written for the chosen account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): boot-timing instrumentation for startup-speed work

Adds a BootTimer module that records milestone timestamps relative to
app.whenReady(), prints an aligned summary on boot completion, and
persists each launch's timings as JSON under userData/boot-timings/
so we can diff across optimisation passes.

Marks (main process):
  whenReady-start            (start of app.whenReady)
  db-bootstrap-done          (after bootstrapRuntimeDatabase)
  ipc-handlers-registered    (just before createMainWindow)
  main-window-created        (BrowserWindow constructed)
  main-window-ready-to-show  (first paint)
  browser-service-ready      (desktop browser subprocess up)
  runtime-spawn-start        (sidecar spawn dispatched)
  runtime-healthy            (first /healthz 200)
  first-list-workspaces      (first listWorkspaces resolves)

Marks (renderer):
  renderer-hydrated          (hasHydratedWorkspaceList flips true →
                              splash unmounts; triggers summary)

Wired through electronAPI.boot.{mark,getTimings} so future renderer-
side milestones (e.g. first-paint-after-data) can be recorded the
same way. No behaviour change; instrumentation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): cut splash 1s — return cached runtime status, drop poll

Two coordinated changes to recover the 1.6s gap measured in baseline
boot timings between runtime-healthy and first-list-workspaces:

B1. runtime:getStatus IPC now returns the cached runtimeStatus
    directly instead of round-tripping through refreshRuntimeStatus()
    which always probes /healthz. During boot the sidecar isn't up
    yet, so each probe ate a 1500ms HTTP timeout. Push events
    (`runtime:state`) keep the cached value live — renderer gets a
    zero-latency stream and the IPC for the same state.

B2. Removed the 1s renderer-side polling loop in workspaceDesktop.tsx
    that re-queried runtime:getStatus while status === "starting".
    The push event already fires the moment the sidecar flips to
    "running"; the poll could only *delay* observed ready by up to
    a full tick (waiting for the next 1s boundary).

Baseline measurement (5.15s total to splash unmount):
  runtime-healthy            3485ms
  first-list-workspaces      5151ms  ← 1.66s gap to fix here

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): hydrate splash from local SQLite — 5.2s → 2.2s

The "Preparing your desktop" splash was gated on the runtime sidecar
spawning + first /api/v1/workspaces query — 3-4s of cold-start work
the user had to stare at on every launch. Three coordinated changes
collapse the wait to under the dev-mode Vite floor (~2.2s; prod will
land closer to ~1s):

1. Decouple workspace load from bootstrap. The workspace-load effect
   used to wait on `isLoadingBootstrap`, which fetches runtimeConfig
   + clientConfig (sidecar IPCs that on cold launch retry against a
   not-yet-healthy sidecar for 1.5s+). None of that bootstrap data is
   needed to render the workspace list. Effect now only gates on
   `runtimeReadyForWorkspaceData`; bootstrap data lands in state in
   the background.

2. Static splash in index.html. Window paints at ~1s but the React
   tree (and its WorkspaceBootstrapPane) used to take another ~450ms
   to mount, leaving a blank window. Inlined matching markup (logo +
   halo + dots animation) so the splash is visible the moment the
   window shows; App.tsx removes the static element via
   useLayoutEffect once React commits — no flash.

3. Optimistic local hydration. Desktop and runtime share runtime.db;
   the desktop main process opens it during bootstrapRuntimeDatabase
   to keep its own workspaces table mirror. New
   listWorkspacesFromLocalDb() reads that mirror directly (read-only
   SQLite handle, ~5-15ms total including IPC), bypassing the sidecar
   entirely. New optimistic-hydration effect on mount fires the cached
   IPC; if any rows come back, splash unmounts immediately. Sidecar
   still spawns + the regular workspace-load effect still runs, doing
   reconciliation in the background. Fresh-install / no-rows case
   falls through to the existing sidecar path, no behaviour change.

Measured (dev mode, cold launch, baseline boot-timing JSON archived):
  before: renderer-hydrated  5151-5984ms (high variance)
  after:  renderer-hydrated  2195ms
  delta:  ~3.6s saved (62%)

Remaining 2.2s is dominated by Vite dev-mode bundle load + React
mount; production builds skip both and should land closer to ~1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): light-mode invisible text from splash CSS body color leak

Splash CSS in index.html set `color: #f5f5f5` on body (chosen via
prefers-color-scheme), which always wins over Tailwind's base
`text-foreground` for elements inheriting from body without an
explicit text-foreground class. When system was dark but app was on
the Holaos light theme, body color stayed near-white → SelectValue
inner span and any other inherited-coloured text went invisible on
white surfaces.

Scope the splash colours to `#boot-splash` itself; let Tailwind's
@apply text-foreground drive body color from the active app theme.

Bonus: SettingsMenuSelectRow forces `data-placeholder:text-foreground`
so the selected value isn't rendered in muted colour while Base UI's
items list is still unregistered (it lazy-mounts inside SelectContent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): drop divider under file identity header in preview

The horizontal line between the file name/meta row and the textarea
content was rendering visibly stronger than intended (and unwanted)
when previewing a file like workspace.yaml. Removed the border-b on
the identity header — content area's own padding already separates
the two sections cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(desktop): drop divider under file identity header in preview"

This reverts commit 0f5715b.

* fix(desktop): tone down brand focus ring + drop boot-timing scaffolding

The global :focus-visible rule painted a 2px brand-coloured outline
plus a 3px box-shadow ring on every input/button/textarea. On full-
pane editors (file preview textarea) the outline rendered as a thick
orange line flush against the surrounding chrome — mistaken for a
divider, hard to ignore.

Switched to a single 2px box-shadow at 50% ring alpha (no outline,
so no offset bleed past overflow boundaries) and removed textarea
from the rule's targets — full-pane editors don't benefit from a
chrome-flush ring, and per-textarea components can opt back in if
they need one.

Also dropped the boot-timing instrumentation that was added during
the splash-speed work — the optimisation has landed, the JSON logs
under userData/boot-timings/ aren't useful day-to-day. Removed:
  - desktop/electron/bootTimer.ts
  - all bootTimer.{start,mark,complete} call sites in main.ts
  - boot:mark / boot:getTimings IPC handlers
  - electronAPI.boot.{mark,getTimings} preload + types
  - 6 renderer-side boot.mark calls in workspaceDesktop.tsx

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
* feat: add token optimization rollout safeguards

- add observability-only context budget telemetry on turn results, including usage, latency, tool payload, browser snapshot, screenshot, compaction, and checkpoint signals
- preserve context budget decisions through state-store migrations, API payloads, and turn memory writeback
- compact large browser/runtime capability tool results for the model while preserving raw payloads in tool details
- add browser_get_state mode, scope, and max_nodes controls with metadata and stable follow-up indexes
- store browser screenshots as workspace output artifacts and return screenshot handles instead of inline base64 when storage is available
- update harness/browser/runtime tests for telemetry, compaction envelopes, scoped browser state, and screenshot artifact recovery
- validation: npm test -- --test-name-pattern "turn results support" (runtime/state-store); npm test -- --test-name-pattern "claimed input persists runner events" (runtime/api-server); npm test -- --test-name-pattern "stores screenshots as output artifacts" (runtime/api-server, full suite); npm test -- --test-name-pattern "Pi desktop browser tools execute" (runtime/harness-host, full suite); npm run typecheck in runtime/state-store, runtime/api-server, and runtime/harness-host

* feat: add general browser toolbox primitives

- add browser_find, browser_act, browser_wait, browser_evaluate, and browser_debug to the shared browser tool surface

- implement locator-based DOM search/action/wait/evaluate/debug execution in the runtime browser service without app-specific shortcuts

- forward browser_space through browser capability status and execution routes so selected browser surfaces are preserved

- document the new general browser tools and expand harness/runtime tests for schemas, routing, and execution

- validate with runtime/api-server typecheck, focused runtime browser tests, runtime/harness-host typecheck, and focused Pi browser tool tests

* fix: strengthen general browser interactions

- accept dom_mutation as a browser_wait alias for DOM-change waits
- detect framework click handlers and pointer-style controls when finding actionable browser elements
- route browser_act pointer actions through a real BrowserView mouse endpoint
- add regression coverage for native mouse input and browser_act forwarding
- validated with runtime/api-server npm run typecheck, runtime/harness-host npm run typecheck, runtime/api-server npm test -- --test-name-pattern desktop browser tool service exposes general find, runtime/harness-host npm test -- --test-name-pattern Pi desktop browser tools expose scoped state, and node --test desktop/electron/browser-context-menu.test.mjs

* fix: prevent browser automation from triggering user interrupt prompts

- suppress browser interrupt prompts while Electron sends trusted internal mouse events for browser tools
- keep the human interrupt flow active for actual user keyboard and mouse input
- accept common browser tool aliases for DOM-change waits and active dialog scopes
- add regression coverage for browser lock suppression and schema alias normalization
- validated with desktop npm run typecheck, runtime/api-server npm run typecheck, runtime/harness-host npm run typecheck, runtime/api-server npm test -- --test-name-pattern desktop browser tool service accepts scoped|desktop browser tool service exposes general find, runtime/harness-host npm test -- --test-name-pattern Pi desktop browser tools execute, and node --test desktop/electron/browser-user-lock.test.mjs desktop/electron/browser-context-menu.test.mjs

* fix: route browser text entry through native input

- add a desktop browser keyboard endpoint for native text insertion, key presses, clear, and submit handling
- route browser_act fill/type/press and legacy browser_type/browser_press through focused native input instead of synthetic DOM mutation
- broaden editable detection for rich editors and allow focused-editor text actions without a locator
- update browser tool guidance and add regression coverage for rich-editor keyboard input
- validation: npm run typecheck in desktop, runtime/api-server, and runtime/harness-host
- validation: node --test desktop/electron/browser-context-menu.test.mjs desktop/electron/browser-user-lock.test.mjs
- validation: node --import tsx --test src/desktop-browser-tools.test.ts --test-name-pattern "desktop browser tool service exposes general find|desktop browser tool service avoids refetching page summaries" in runtime/api-server
- validation: npm test -- --test-name-pattern "Pi desktop browser tools execute" in runtime/harness-host

* chore: refresh desktop package lock metadata

- refresh desktop package-lock metadata for npm peer and optional dependency flags

- include generated optional dependency entries required by the current lockfile state

- validation: parsed desktop/package-lock.json with Node JSON.parse
- allow about:blank and HTTP/S auth-style popup windows before external URL handoff
- keep popup windows on the workspace browser session so login state is shared
- wrap main-process external URL opens to avoid unhandled promise rejections
- update browser tab policy coverage for popup and external URL behavior

Validation:
- node --test desktop/electron/browser-tab-policy.test.mjs
- npm --prefix desktop run typecheck
- git diff --check
- remove browser comment buttons from the standalone and space browser toolbars
- delete the browser comment capture IPC, preload API, renderer types, and injected overlay script
- remove AppShell and ChatPane browser-comment draft routing
- update source-regex coverage to assert browser comments are no longer exposed

Validation:
- npm --prefix desktop run typecheck
- node --test --test-name-pattern "desktop browser does not expose injected comment capture|browser pane exposes screenshot copy without browser comments|space browser display exposes screenshot copy without browser comments|app shell does not route browser comment captures into chat attachments|chat pane does not expose browser comment draft plumbing" desktop/electron/browser-capture-annotation.test.mjs desktop/src/components/panes/BrowserPane.test.mjs desktop/src/components/panes/SpaceBrowserDisplayPane.test.mjs desktop/src/components/layout/AppShell.test.mjs desktop/src/components/panes/ChatPaneComposerPrefill.test.mjs
- git diff --check
- add a workspace picker to the About diagnostics panel and pass the selected workspace through the Electron bridge
- resolve the selected workspace in the main process and include workspace metadata in the bundle filename, summary, and renderer result
- export a workspace-scoped runtime database snapshot with workspace metadata while preserving logs and redacted runtime config
- update diagnostics export regression coverage for the workspace-aware IPC and bundle wiring
- validation: node --test desktop/electron/diagnostics-export.test.mjs; npm run desktop:typecheck; git diff --check
- prevent workspace-level persisted Pi session files from being reused when a turn already has an explicit per-session harness id
- make Pi treat stale explicit harness session ids as fresh sessions instead of falling back to another persisted session file
- update regression tests for bootstrap routing and Pi prompt resume behavior
- validated with node --import tsx --test src/ts-runner.test.ts, node --import tsx --test src/pi.test.ts, and npm run typecheck in runtime/api-server and runtime/harness-host
- add Holaboss Search as a managed desktop web search provider without endpoint or API-key fields
- route native web_search through runtime-configured Holaboss bindings and send user, sandbox, and tool-call headers
- derive local search service URLs from control-plane or model-proxy runtime config for development sessions
- document web_search runtime config and provider options
- add targeted native and Pi web search tests for managed Holaboss routing and local service URL derivation
- validated with node --import tsx --test src/native-web-search.test.ts, node --import tsx --test src/pi-web-search.test.ts, npm --prefix runtime/api-server run typecheck, npm --prefix runtime/harness-host run typecheck, npm --prefix desktop run typecheck
- increase the full model selector width budget so composer controls enter compact mode at the expected pane width
- make the compact thinking selector render icon-only once measured compact width is active
- update composer source assertions to cover the minimized thinking selector behavior
- validation: node --test --test-name-pattern "chat composer footer wraps|chat composer switches model and thinking" desktop/src/components/panes/ChatPane.test.mjs
- validation: node --test desktop/src/components/panes/ChatPaneReasoningControl.test.mjs
- validation: npm --prefix desktop run typecheck
- validation: git diff --check
…orkspace-scoped bindings) (holaboss-ai#218)

* chore: gitignore website/docs build outputs

The docs-next site (TanStack Start + Fumadocs) leaves .react-router/,
.source/, and build/ in website/docs/ on every build, which keeps
nagging in git status. Match the existing pattern used for the
.vitepress cache/dist directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(integrations): user-global connections + reverse migration + owner_user_id check

Connections are user-global; per-workspace scoping lives entirely in
integration_bindings. The short-lived feat/composio-workspace-scoped-accounts
branch added a workspace_id column to integration_connections that conflicts
with the "one account → many workspaces" requirement.

- state-store: add migrateRevertIntegrationConnectionsWorkspace, which
  detects the legacy column, materializes each non-null workspace_id row
  into a default workspace binding (skipping rows that already have one),
  drops the now-stale workspace-provider index, and ALTER TABLE DROP
  COLUMN workspace_id (SQLite ≥ 3.35).
- state-store: regression tests covering legacy-with-data upgrade
  (3 mixed scenarios) and fresh-DB no-op.
- api-server: add resolveOwnerUserId helper that, when HOLABOSS_USER_ID is
  set (managed/sandbox), rejects bodies whose owner_user_id mismatches.
  In OSS local mode (env unset) behaviour is unchanged.
- api-server: apply ownerCheck to POST /api/v1/integrations/connections
  and /api/v1/integrations/composio/finalize.
- api-server: composio/finalize now accepts an optional workspace_id and,
  when present, atomically upserts a default workspace binding alongside
  the new connection — the connection itself remains user-global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-account Settings → Integrations + per-app binding selector

- Settings → Integrations now lists every active connection per provider
  (no provider-level dedup), each as its own row with its own disconnect
  button. Replaces the old "one Connected pill per provider" UX that
  silently hid additional accounts. The Connect button on a connected
  card now reads "Connect another <provider> account".
- AppSurfacePane's read-only "Connected/Not connected" pill becomes a
  Select dropdown of every active account for the app's expected
  provider. Picking one upserts an app-level integration_binding so this
  workspace's copy of the app uses that account; switching workspaces
  or apps preserves each binding independently. The dropdown's "Connect
  new account" entry opens Settings → Integrations.
- Empty state when the user has no accounts for the provider: a Connect
  CTA that opens Settings → Integrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): tighter Settings → Integrations card + use Composio profile metadata

The previous ConnectedProviderCard was loud — provider header with logo
+ name + "N accounts" subtitle, each account in its own bordered box
with a green status dot, plus a full-width "Connect another" footer
button. Compress to:

- Header line: small (size-5) provider logo + name + an icon-sized
  Connect-another button on the right. Drop the redundant account count.
- Account row: size-3.5 avatar + handle + disconnect icon, no border or
  status dot. The row's mere presence implies "connected".

Account label/avatar now come from Composio profile metadata:
- Renderer fetches composioAccountStatus(account_external_id) for each
  visible connection in parallel after loadData and stores results in a
  Map<connectionId, ComposioAccountStatus>. Append-only across loads so
  transient fetch failures don't blank avatars already on screen.
- Display priority: handle (prefixed @ if missing) → email → displayName
  → account_label (skipping the boilerplate "(Managed)" suffix) →
  account_external_id → connection_id.
- ComposioAccountStatus type extended in both desktop/electron/main.ts
  and desktop/src/types/electron.d.ts with optional handle, displayName,
  avatarUrl, email, and the raw data passthrough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): drop avatars and Composio-id fallback from account rows

Avatars only render reliably for some providers (Twitter has them,
Reddit and LinkedIn often don't), and the empty grey placeholder noise
isn't worth keeping just for the providers that do. Drop the avatar
column entirely.

When metadata is missing the previous fallback chain ended at
account_external_id — but for managed Composio accounts that's the
"ca_xxx" connected_account_id, which tells the user nothing. Skip any
label that starts with "ca_" and fall through to a stable "Account N"
index instead. The metadata fetch still upgrades the label to a real
@handle / email / displayName once Hono /composio/account/:id starts
returning the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): bring back size-3.5 account avatars

Now that Composio whoami delivers reliable avatar URLs across all
managed providers (Gmail, GitHub, Twitter, LinkedIn, Reddit), put the
avatar back on each account row. When the URL hasn't loaded yet (or a
new provider lacks one), fall through to a tiny lettered placeholder
keyed off the display label's first character — matches the avatar's
size-3.5 rounded-full footprint so the row layout stays stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): cascade delete connection bindings + Google avatar referrer

Two bugs from real-world testing of Settings → Integrations:

1. Disconnect button did nothing for any account that was already used
   in a workspace. The runtime's deleteConnection threw 409 ("connection
   is bound to N workspaces — remove bindings first") and the desktop
   set a barely-visible status message. But the user-global model is
   "one account → many workspaces"; deleting an account is the explicit
   way to detach it from all workspaces, not an error condition.
   Cascade-delete every binding (orphan + live) pointing at the
   connection before dropping the row, and report removed_bindings on
   the response so the UI can mention it later if we want a confirm.
   Updated the test that asserted the old 409 path.

2. Google profile avatars (lh3.googleusercontent.com) rendered as broken
   image icons in Electron. The CDN rejects requests whose Referer
   header includes the renderer's localhost dev origin (and even some
   prod origins for hotlink protection). Setting referrerPolicy to
   "no-referrer" on the avatar img drops the header — provider CDNs are
   uniformly happy with no referrer and the image loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): degrade to letter placeholder when avatar URL fails to load

Provider CDNs can be flaky (Twitter rate-limits, LinkedIn auth quirks),
which today shows up as a broken-image icon next to the row. Track
failed-to-load URLs in component-local state and swap to the same
size-3.5 lettered placeholder we already use when avatarUrl is missing
entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): provider logos missing for Gmail/Sheets/Twitter cards

Two stacked bugs in mergeIntegrationCards left integration.logo null
for several connected providers — the screenshot showed grey letter
placeholders instead of the brand mark for Gmail, Google Sheets, and
Twitter / X:

1. TOOLKIT_SLUG_TO_PROVIDER collapses gmail and googlesheets onto
   provider key "google", so the catalog entries with provider_id
   "gmail" / "googlesheets" miss the toolkitByProvider lookup.

2. Hono /composio/toolkits filters out toolkits whose
   composio_managed_auth_schemes is empty unless the user's namespace
   has a custom auth_config for them. Twitter currently has empty
   managed schemes Composio-side, so a user without a custom Twitter
   auth_config never sees the toolkit reach mergeIntegrationCards.

Both root causes are fragile. Sidestep by falling back to Composio's
public logo CDN — `https://logos.composio.dev/api/{slug}` is a stable
public URL keyed by the toolkit slug, no auth required, returns SVG
for every supported provider including Gmail/Sheets/Twitter. Wired in
when toolkit?.logo is null so we still prefer Composio's curated
toolkit logo when present.

Also added the same referrerPolicy="no-referrer" + onError → letter
fallback we use for account avatars, so a logo CDN hiccup degrades
gracefully instead of showing a broken-image icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): UI polish for SettingsDialog + TopTabsBar

- SettingsDialog: arbitrary Tailwind values normalized to scale
  utilities (min-w-[140px] → min-w-35, etc.), Select triggers gain a
  visible border instead of border-transparent, break-words → wrap-
  break-word, plus a few formatter reflows.
- TopTabsBar: user dropdown menu icons match the rest of the app's
  icon-in-menu convention (opacity-60 size-3.5), and useDesktopBilling
  destructure reformatted for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plans): manifest-driven integration routing plan (revised)

Revises the original "yaml-driven" draft after design review:

- Source of truth flips from `app.runtime.yaml` to `marketplace.json`,
  with cross-source consistency enforced by sync endpoint + CI lint
  (closes the silent-drift class this design exists to eliminate).
- D1 / D2 / D3 decisions are surfaced as a "Decisions required BEFORE
  Phase 1" section instead of being scattered through Risks.
- New Phase 0 — Composio byo-creds spike — gates Phase 1 schema. Spike
  script lives in `hola-boss-apps/scripts/composio-spike-byo-creds.ts`
  (separate commit in that repo).
- Phase 2 splits into 2A (helper + dual-source fallback, no entries
  removed) and 2B (per-app cutover with reconciler surfacing badges as
  each app flips). Reconciler ships as no-op code in 2A, becomes
  load-bearing in 2B per-app.
- Phase 3 prerequisites tightened: `<CredentialsModal>` UX wireframe
  added (error display, multi-account, post-connect verify, a11y).
- SDK regen step in Phase 1 explicitly notes both desktop AND
  `frontend/apps/web` consume the regenerated `@holaboss/app-sdk`.
- `_template` integration switched from "comment block in README" to
  "extend `create-hola-app` to emit `marketplace.entry.json` into the
  new app's directory" — comments rot, CLI artifacts don't.
- "No PR to holaOS necessary" claim narrowed to "no PR under the
  existing auth modes" (new auth_mode obviously requires holaOS code).
- File name kept (`yaml-driven-...`) to preserve any external
  references; H1 reads "manifest-driven integration routing".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "docs(plans): manifest-driven integration routing plan (revised)"

This reverts commit f89ad79.

* fix(desktop): unblock Composio connect and unify app icons

The integration connect flow had three layered failures that compounded
into the reviewer-blocking "Connected account not found" loop:

1. composioFetch hard-failed up front when the Better Auth cookie was
   missing/invalid (throwing "Not authenticated"), bypassing the 401
   re-auth retry that requestControlPlaneJson already had. Extracted a
   shared retryAfterSessionAuth helper (single-flight; coalesces parallel
   401s into one sign-in browser window) and routed both call sites
   through it.

2. Node fetch (undici) reused half-closed keep-alive sockets to
   api.imerchstaging.com, surfacing as ECONNRESET right when the polling
   loop hit the staging endpoint hardest. Added fetchWithNetworkRetry
   that catches ECONNRESET / ETIMEDOUT / SocketError on the first attempt
   and retries once after a 200ms backoff with a fresh connection.

3. The polling loop blindly trusted the connected_account_id returned
   by /api/composio/connect — but that id is not queryable via Composio
   v3's /connected_accounts/{id} endpoint until well after OAuth
   completes (and in some cases never resolves). Replaced the by-id poll
   with a snapshot-based approach: capture the user's current connection
   ids before /connect, then poll /api/composio/connections (a stable v3
   endpoint) for any *new* id matching the toolkit. Applied across all
   four call sites (IntegrationsPane, MarketplacePane, FirstWorkspacePane,
   workspaceDesktop). Added a composioListConnections IPC + preload
   binding to support the new flow.

Side fixes bundled because they came up while debugging the same flow:

- composioListToolkits no longer silently returns an empty toolkit list
  on missing cookie (the prior behaviour masked auth issues as "no
  integrations available")
- New AppIcon component centralises the icon resolver as a 4-stage
  fallback: backend manifest URL → Composio CDN → hardcoded SVG kit →
  letter chip. CDN is preferred over local SVGs because its flat brand
  marks render at uniform sizes, eliminating the visual drift between
  cartoon-style Reddit/Octocat-style GitHub and HubSpot-style letter
  placeholder. Adopted in AppCatalogCard and SpaceApplicationsExplorerPane
- Provider display names extended to cover hubspot/attio/calcom/apollo/
  instantly/zoominfo
- Session search dropdown gets 2px gap between rows for breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): track unavailable MCP servers in pi harness

Surface MCP server unavailability through the harness host so the
desktop and api-server tier can render warnings (vs. silently dropping
tools when an MCP backend is down). Adds:

- PiMcpServerUnavailableInfo type carrying { serverId, reason,
  missingToolIds } per failed server
- unavailableServers field on PiMcpToolset and PiSessionHandle so the
  toolset builder can hand a structured summary back to callers
- mcp_server_unavailable event type in both KnownRunnerEventType
  (harness-host contract) and TsRunnerEventType (api-server contract)
  so consumers can listen for these mid-run

No behaviour change for runs where every MCP server starts cleanly —
unavailableServers stays empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): dedupe re-auth flows + install-time account selector

Two bugs reported on PR holaboss-ai#218 once multi-account landed:

  1. Same external account connected twice shows up as two rows. Composio
     re-auth mints a new connected_account_id even for the same real
     account, and the upsert was keyed on connection_id only — every
     re-auth = fresh row. Pre-fix legacy rows had no provider-side
     identity persisted, so they were invisible to any retroactive
     dedupe attempt either.

  2. No way to choose which account to bind when installing an app.
     installAppFromCatalog auto-picked the first active connection for
     the app's expected provider; multi-account users had to install
     first, then go to AppSurfacePane to switch.

Phase 1 — dedupe-on-finalize
- state-store: integration_connections gets account_handle + account_email
  columns (additive migration; legacy DBs heal as users reconnect).
  New findActiveIntegrationConnectionByIdentity finder, scoped per
  (provider, owner), case-insensitive on either field.
- api-server: createConnection looks for an active match by identity
  before creating a fresh row. On hit it preserves connection_id so
  every integration_binding pointing at it stays valid; only volatile
  fields (external_id, secret_ref, label, scopes) refresh.
- desktop main: composioFinalize wrapper does whoami before posting and
  threads handle/email/displayName through. Identity propagation is
  transparent to the four renderer call sites.

Phase 2 — install-time account selector
- AppCatalogCard accepts availableAccounts / selectedConnectionId /
  onSelectAccount. Inline Select renders only when ≥ 2 accounts; one
  account = silent auto-bind; zero = existing "connect first" dialog.
- workspaceDesktop exports APP_TO_PROVIDER_MAP + getProviderForApp;
  installAppFromCatalog accepts an optional connectionId and writes
  integration_bindings (target_type=app, target_id=appId) atomically
  after the app install succeeds.
- AppsGallery groups active connections by provider, tracks per-card
  selection in local state, refreshes after install completes.
- AppSurfacePane integration picker reworked: provider-icon trigger
  with handle as primary line and email / label as secondary
  disambiguation, big enough to read multi-account flows at a glance.

Phase 3 — retroactive merge for users already affected
- Legacy NULL-identity rows were unreachable to the dedupe finder, so
  re-auth still spawned duplicates after Phase 1. Two new escape hatches:
  - composioFinalize backfill: before posting, list (provider, owner)
    rows with NULL identity, whoami each via Composio, PATCH the result
    back so dedupe can match on the next finalize.
  - IntegrationsPane auto-reconcile: after enrichment populates
    accountMetadata for *all* probeable rows, group by (provider,
    resolved-identity); for any group with ≥ 2 rows, keep the oldest,
    backfill identity, call merge to repoint bindings + drop the rest.
    Guarded with reconcileInFlightRef + cancellation flag for unmount.
- New runtime mergeConnections({ keepConnectionId, removeConnectionIds })
  validates (provider, owner) match, repoints every binding inside a
  SQLite transaction (atomic — half-merges can't survive a crash),
  rejects cross-provider/owner merges so unrelated accounts can't
  accidentally absorb each other's bindings.
- New routes: PATCH /connections/:id (now identity-aware) +
  POST /connections/:id/merge.

Misc
- workspaceApps.ts drops the static AppToolDefinition arrays — every
  app's MCP server already advertises tools dynamically; the snapshots
  drifted whenever an app shipped new ones. AppSurfacePane drops the
  "Tools (N)" section that consumed them.

Tests
- state-store: identity columns persist + null when missing,
  findActiveIntegrationConnectionByIdentity (provider/owner scoping,
  case-insensitive, skip inactive, both-keys-supplied), legacy DB
  migration path adds the columns + indexes idempotently. 70/70.
- api-server: dedupe-on-reconnect preserves connection_id and bindings,
  no-identity rows still create fresh, dedupe is provider-scoped,
  identity backfill via updateConnection, merge repoints across
  multiple workspaces, merge rejects cross-provider. 501/501.

Manual smoke (against my local DB with the bug active):
- 2 @jotyy GitHub rows on launch → IntegrationsPane reconcile fires
  after enrichment → merged into 1 row, both prior bindings preserved.
- New re-auth → no duplicate row, existing row's external_id refreshed.
- Marketplace install with 2 active accounts → inline picker, default
  most-recent, binding written for the chosen account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): refresh integration connections on Reload + window focus

The "Connect my account" CTA only fetched the connection list once on
mount, so users who reconnected an account in Settings and came back to
the app pane saw a stale empty list. Clicking Reload only swapped the
iframe key — checkIntegration never ran again.

- Reload + frame-error Retry now refetch integrations alongside the
  iframe reload.
- Adds a window-focus + visibilitychange listener (3s throttled) so the
  common OAuth path (Holaboss → browser → back) picks up the new
  connection automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): remount AppSurfacePane on appId change

AppSurfacePane carried local state (wasRemoved, frameUrl, frameLoading,
frameError, integrationContext) across appId changes because the two
render sites in AppShell didn't pass a key. Switching from one app to
another reused the same fiber, so stale state from the previous app
could leave the new app's pane visually empty until the user navigated
away (forcing an unmount) and back.

Adding key={appId} forces a fresh mount per app, matching the workflow
that already worked (switch to browser/files and back). Iframe reload
behavior is unchanged — the iframe was already keyed on frameUrl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
…aboss-ai#226)

* chore: gitignore website/docs build outputs

The docs-next site (TanStack Start + Fumadocs) leaves .react-router/,
.source/, and build/ in website/docs/ on every build, which keeps
nagging in git status. Match the existing pattern used for the
.vitepress cache/dist directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(integrations): user-global connections + reverse migration + owner_user_id check

Connections are user-global; per-workspace scoping lives entirely in
integration_bindings. The short-lived feat/composio-workspace-scoped-accounts
branch added a workspace_id column to integration_connections that conflicts
with the "one account → many workspaces" requirement.

- state-store: add migrateRevertIntegrationConnectionsWorkspace, which
  detects the legacy column, materializes each non-null workspace_id row
  into a default workspace binding (skipping rows that already have one),
  drops the now-stale workspace-provider index, and ALTER TABLE DROP
  COLUMN workspace_id (SQLite ≥ 3.35).
- state-store: regression tests covering legacy-with-data upgrade
  (3 mixed scenarios) and fresh-DB no-op.
- api-server: add resolveOwnerUserId helper that, when HOLABOSS_USER_ID is
  set (managed/sandbox), rejects bodies whose owner_user_id mismatches.
  In OSS local mode (env unset) behaviour is unchanged.
- api-server: apply ownerCheck to POST /api/v1/integrations/connections
  and /api/v1/integrations/composio/finalize.
- api-server: composio/finalize now accepts an optional workspace_id and,
  when present, atomically upserts a default workspace binding alongside
  the new connection — the connection itself remains user-global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-account Settings → Integrations + per-app binding selector

- Settings → Integrations now lists every active connection per provider
  (no provider-level dedup), each as its own row with its own disconnect
  button. Replaces the old "one Connected pill per provider" UX that
  silently hid additional accounts. The Connect button on a connected
  card now reads "Connect another <provider> account".
- AppSurfacePane's read-only "Connected/Not connected" pill becomes a
  Select dropdown of every active account for the app's expected
  provider. Picking one upserts an app-level integration_binding so this
  workspace's copy of the app uses that account; switching workspaces
  or apps preserves each binding independently. The dropdown's "Connect
  new account" entry opens Settings → Integrations.
- Empty state when the user has no accounts for the provider: a Connect
  CTA that opens Settings → Integrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): tighter Settings → Integrations card + use Composio profile metadata

The previous ConnectedProviderCard was loud — provider header with logo
+ name + "N accounts" subtitle, each account in its own bordered box
with a green status dot, plus a full-width "Connect another" footer
button. Compress to:

- Header line: small (size-5) provider logo + name + an icon-sized
  Connect-another button on the right. Drop the redundant account count.
- Account row: size-3.5 avatar + handle + disconnect icon, no border or
  status dot. The row's mere presence implies "connected".

Account label/avatar now come from Composio profile metadata:
- Renderer fetches composioAccountStatus(account_external_id) for each
  visible connection in parallel after loadData and stores results in a
  Map<connectionId, ComposioAccountStatus>. Append-only across loads so
  transient fetch failures don't blank avatars already on screen.
- Display priority: handle (prefixed @ if missing) → email → displayName
  → account_label (skipping the boilerplate "(Managed)" suffix) →
  account_external_id → connection_id.
- ComposioAccountStatus type extended in both desktop/electron/main.ts
  and desktop/src/types/electron.d.ts with optional handle, displayName,
  avatarUrl, email, and the raw data passthrough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): drop avatars and Composio-id fallback from account rows

Avatars only render reliably for some providers (Twitter has them,
Reddit and LinkedIn often don't), and the empty grey placeholder noise
isn't worth keeping just for the providers that do. Drop the avatar
column entirely.

When metadata is missing the previous fallback chain ended at
account_external_id — but for managed Composio accounts that's the
"ca_xxx" connected_account_id, which tells the user nothing. Skip any
label that starts with "ca_" and fall through to a stable "Account N"
index instead. The metadata fetch still upgrades the label to a real
@handle / email / displayName once Hono /composio/account/:id starts
returning the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): bring back size-3.5 account avatars

Now that Composio whoami delivers reliable avatar URLs across all
managed providers (Gmail, GitHub, Twitter, LinkedIn, Reddit), put the
avatar back on each account row. When the URL hasn't loaded yet (or a
new provider lacks one), fall through to a tiny lettered placeholder
keyed off the display label's first character — matches the avatar's
size-3.5 rounded-full footprint so the row layout stays stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): cascade delete connection bindings + Google avatar referrer

Two bugs from real-world testing of Settings → Integrations:

1. Disconnect button did nothing for any account that was already used
   in a workspace. The runtime's deleteConnection threw 409 ("connection
   is bound to N workspaces — remove bindings first") and the desktop
   set a barely-visible status message. But the user-global model is
   "one account → many workspaces"; deleting an account is the explicit
   way to detach it from all workspaces, not an error condition.
   Cascade-delete every binding (orphan + live) pointing at the
   connection before dropping the row, and report removed_bindings on
   the response so the UI can mention it later if we want a confirm.
   Updated the test that asserted the old 409 path.

2. Google profile avatars (lh3.googleusercontent.com) rendered as broken
   image icons in Electron. The CDN rejects requests whose Referer
   header includes the renderer's localhost dev origin (and even some
   prod origins for hotlink protection). Setting referrerPolicy to
   "no-referrer" on the avatar img drops the header — provider CDNs are
   uniformly happy with no referrer and the image loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): degrade to letter placeholder when avatar URL fails to load

Provider CDNs can be flaky (Twitter rate-limits, LinkedIn auth quirks),
which today shows up as a broken-image icon next to the row. Track
failed-to-load URLs in component-local state and swap to the same
size-3.5 lettered placeholder we already use when avatarUrl is missing
entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): provider logos missing for Gmail/Sheets/Twitter cards

Two stacked bugs in mergeIntegrationCards left integration.logo null
for several connected providers — the screenshot showed grey letter
placeholders instead of the brand mark for Gmail, Google Sheets, and
Twitter / X:

1. TOOLKIT_SLUG_TO_PROVIDER collapses gmail and googlesheets onto
   provider key "google", so the catalog entries with provider_id
   "gmail" / "googlesheets" miss the toolkitByProvider lookup.

2. Hono /composio/toolkits filters out toolkits whose
   composio_managed_auth_schemes is empty unless the user's namespace
   has a custom auth_config for them. Twitter currently has empty
   managed schemes Composio-side, so a user without a custom Twitter
   auth_config never sees the toolkit reach mergeIntegrationCards.

Both root causes are fragile. Sidestep by falling back to Composio's
public logo CDN — `https://logos.composio.dev/api/{slug}` is a stable
public URL keyed by the toolkit slug, no auth required, returns SVG
for every supported provider including Gmail/Sheets/Twitter. Wired in
when toolkit?.logo is null so we still prefer Composio's curated
toolkit logo when present.

Also added the same referrerPolicy="no-referrer" + onError → letter
fallback we use for account avatars, so a logo CDN hiccup degrades
gracefully instead of showing a broken-image icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): UI polish for SettingsDialog + TopTabsBar

- SettingsDialog: arbitrary Tailwind values normalized to scale
  utilities (min-w-[140px] → min-w-35, etc.), Select triggers gain a
  visible border instead of border-transparent, break-words → wrap-
  break-word, plus a few formatter reflows.
- TopTabsBar: user dropdown menu icons match the rest of the app's
  icon-in-menu convention (opacity-60 size-3.5), and useDesktopBilling
  destructure reformatted for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plans): manifest-driven integration routing plan (revised)

Revises the original "yaml-driven" draft after design review:

- Source of truth flips from `app.runtime.yaml` to `marketplace.json`,
  with cross-source consistency enforced by sync endpoint + CI lint
  (closes the silent-drift class this design exists to eliminate).
- D1 / D2 / D3 decisions are surfaced as a "Decisions required BEFORE
  Phase 1" section instead of being scattered through Risks.
- New Phase 0 — Composio byo-creds spike — gates Phase 1 schema. Spike
  script lives in `hola-boss-apps/scripts/composio-spike-byo-creds.ts`
  (separate commit in that repo).
- Phase 2 splits into 2A (helper + dual-source fallback, no entries
  removed) and 2B (per-app cutover with reconciler surfacing badges as
  each app flips). Reconciler ships as no-op code in 2A, becomes
  load-bearing in 2B per-app.
- Phase 3 prerequisites tightened: `<CredentialsModal>` UX wireframe
  added (error display, multi-account, post-connect verify, a11y).
- SDK regen step in Phase 1 explicitly notes both desktop AND
  `frontend/apps/web` consume the regenerated `@holaboss/app-sdk`.
- `_template` integration switched from "comment block in README" to
  "extend `create-hola-app` to emit `marketplace.entry.json` into the
  new app's directory" — comments rot, CLI artifacts don't.
- "No PR to holaOS necessary" claim narrowed to "no PR under the
  existing auth modes" (new auth_mode obviously requires holaOS code).
- File name kept (`yaml-driven-...`) to preserve any external
  references; H1 reads "manifest-driven integration routing".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "docs(plans): manifest-driven integration routing plan (revised)"

This reverts commit f89ad79afd4b709ec2e96d7d233a77c724e80ba5.

* fix(desktop): unblock Composio connect and unify app icons

The integration connect flow had three layered failures that compounded
into the reviewer-blocking "Connected account not found" loop:

1. composioFetch hard-failed up front when the Better Auth cookie was
   missing/invalid (throwing "Not authenticated"), bypassing the 401
   re-auth retry that requestControlPlaneJson already had. Extracted a
   shared retryAfterSessionAuth helper (single-flight; coalesces parallel
   401s into one sign-in browser window) and routed both call sites
   through it.

2. Node fetch (undici) reused half-closed keep-alive sockets to
   api.imerchstaging.com, surfacing as ECONNRESET right when the polling
   loop hit the staging endpoint hardest. Added fetchWithNetworkRetry
   that catches ECONNRESET / ETIMEDOUT / SocketError on the first attempt
   and retries once after a 200ms backoff with a fresh connection.

3. The polling loop blindly trusted the connected_account_id returned
   by /api/composio/connect — but that id is not queryable via Composio
   v3's /connected_accounts/{id} endpoint until well after OAuth
   completes (and in some cases never resolves). Replaced the by-id poll
   with a snapshot-based approach: capture the user's current connection
   ids before /connect, then poll /api/composio/connections (a stable v3
   endpoint) for any *new* id matching the toolkit. Applied across all
   four call sites (IntegrationsPane, MarketplacePane, FirstWorkspacePane,
   workspaceDesktop). Added a composioListConnections IPC + preload
   binding to support the new flow.

Side fixes bundled because they came up while debugging the same flow:

- composioListToolkits no longer silently returns an empty toolkit list
  on missing cookie (the prior behaviour masked auth issues as "no
  integrations available")
- New AppIcon component centralises the icon resolver as a 4-stage
  fallback: backend manifest URL → Composio CDN → hardcoded SVG kit →
  letter chip. CDN is preferred over local SVGs because its flat brand
  marks render at uniform sizes, eliminating the visual drift between
  cartoon-style Reddit/Octocat-style GitHub and HubSpot-style letter
  placeholder. Adopted in AppCatalogCard and SpaceApplicationsExplorerPane
- Provider display names extended to cover hubspot/attio/calcom/apollo/
  instantly/zoominfo
- Session search dropdown gets 2px gap between rows for breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): track unavailable MCP servers in pi harness

Surface MCP server unavailability through the harness host so the
desktop and api-server tier can render warnings (vs. silently dropping
tools when an MCP backend is down). Adds:

- PiMcpServerUnavailableInfo type carrying { serverId, reason,
  missingToolIds } per failed server
- unavailableServers field on PiMcpToolset and PiSessionHandle so the
  toolset builder can hand a structured summary back to callers
- mcp_server_unavailable event type in both KnownRunnerEventType
  (harness-host contract) and TsRunnerEventType (api-server contract)
  so consumers can listen for these mid-run

No behaviour change for runs where every MCP server starts cleanly —
unavailableServers stays empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): dedupe re-auth flows + install-time account selector

Two bugs reported on PR #218 once multi-account landed:

  1. Same external account connected twice shows up as two rows. Composio
     re-auth mints a new connected_account_id even for the same real
     account, and the upsert was keyed on connection_id only — every
     re-auth = fresh row. Pre-fix legacy rows had no provider-side
     identity persisted, so they were invisible to any retroactive
     dedupe attempt either.

  2. No way to choose which account to bind when installing an app.
     installAppFromCatalog auto-picked the first active connection for
     the app's expected provider; multi-account users had to install
     first, then go to AppSurfacePane to switch.

Phase 1 — dedupe-on-finalize
- state-store: integration_connections gets account_handle + account_email
  columns (additive migration; legacy DBs heal as users reconnect).
  New findActiveIntegrationConnectionByIdentity finder, scoped per
  (provider, owner), case-insensitive on either field.
- api-server: createConnection looks for an active match by identity
  before creating a fresh row. On hit it preserves connection_id so
  every integration_binding pointing at it stays valid; only volatile
  fields (external_id, secret_ref, label, scopes) refresh.
- desktop main: composioFinalize wrapper does whoami before posting and
  threads handle/email/displayName through. Identity propagation is
  transparent to the four renderer call sites.

Phase 2 — install-time account selector
- AppCatalogCard accepts availableAccounts / selectedConnectionId /
  onSelectAccount. Inline Select renders only when ≥ 2 accounts; one
  account = silent auto-bind; zero = existing "connect first" dialog.
- workspaceDesktop exports APP_TO_PROVIDER_MAP + getProviderForApp;
  installAppFromCatalog accepts an optional connectionId and writes
  integration_bindings (target_type=app, target_id=appId) atomically
  after the app install succeeds.
- AppsGallery groups active connections by provider, tracks per-card
  selection in local state, refreshes after install completes.
- AppSurfacePane integration picker reworked: provider-icon trigger
  with handle as primary line and email / label as secondary
  disambiguation, big enough to read multi-account flows at a glance.

Phase 3 — retroactive merge for users already affected
- Legacy NULL-identity rows were unreachable to the dedupe finder, so
  re-auth still spawned duplicates after Phase 1. Two new escape hatches:
  - composioFinalize backfill: before posting, list (provider, owner)
    rows with NULL identity, whoami each via Composio, PATCH the result
    back so dedupe can match on the next finalize.
  - IntegrationsPane auto-reconcile: after enrichment populates
    accountMetadata for *all* probeable rows, group by (provider,
    resolved-identity); for any group with ≥ 2 rows, keep the oldest,
    backfill identity, call merge to repoint bindings + drop the rest.
    Guarded with reconcileInFlightRef + cancellation flag for unmount.
- New runtime mergeConnections({ keepConnectionId, removeConnectionIds })
  validates (provider, owner) match, repoints every binding inside a
  SQLite transaction (atomic — half-merges can't survive a crash),
  rejects cross-provider/owner merges so unrelated accounts can't
  accidentally absorb each other's bindings.
- New routes: PATCH /connections/:id (now identity-aware) +
  POST /connections/:id/merge.

Misc
- workspaceApps.ts drops the static AppToolDefinition arrays — every
  app's MCP server already advertises tools dynamically; the snapshots
  drifted whenever an app shipped new ones. AppSurfacePane drops the
  "Tools (N)" section that consumed them.

Tests
- state-store: identity columns persist + null when missing,
  findActiveIntegrationConnectionByIdentity (provider/owner scoping,
  case-insensitive, skip inactive, both-keys-supplied), legacy DB
  migration path adds the columns + indexes idempotently. 70/70.
- api-server: dedupe-on-reconnect preserves connection_id and bindings,
  no-identity rows still create fresh, dedupe is provider-scoped,
  identity backfill via updateConnection, merge repoints across
  multiple workspaces, merge rejects cross-provider. 501/501.

Manual smoke (against my local DB with the bug active):
- 2 @jotyy GitHub rows on launch → IntegrationsPane reconcile fires
  after enrichment → merged into 1 row, both prior bindings preserved.
- New re-auth → no duplicate row, existing row's external_id refreshed.
- Marketplace install with 2 active accounts → inline picker, default
  most-recent, binding written for the chosen account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): boot-timing instrumentation for startup-speed work

Adds a BootTimer module that records milestone timestamps relative to
app.whenReady(), prints an aligned summary on boot completion, and
persists each launch's timings as JSON under userData/boot-timings/
so we can diff across optimisation passes.

Marks (main process):
  whenReady-start            (start of app.whenReady)
  db-bootstrap-done          (after bootstrapRuntimeDatabase)
  ipc-handlers-registered    (just before createMainWindow)
  main-window-created        (BrowserWindow constructed)
  main-window-ready-to-show  (first paint)
  browser-service-ready      (desktop browser subprocess up)
  runtime-spawn-start        (sidecar spawn dispatched)
  runtime-healthy            (first /healthz 200)
  first-list-workspaces      (first listWorkspaces resolves)

Marks (renderer):
  renderer-hydrated          (hasHydratedWorkspaceList flips true →
                              splash unmounts; triggers summary)

Wired through electronAPI.boot.{mark,getTimings} so future renderer-
side milestones (e.g. first-paint-after-data) can be recorded the
same way. No behaviour change; instrumentation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): cut splash 1s — return cached runtime status, drop poll

Two coordinated changes to recover the 1.6s gap measured in baseline
boot timings between runtime-healthy and first-list-workspaces:

B1. runtime:getStatus IPC now returns the cached runtimeStatus
    directly instead of round-tripping through refreshRuntimeStatus()
    which always probes /healthz. During boot the sidecar isn't up
    yet, so each probe ate a 1500ms HTTP timeout. Push events
    (`runtime:state`) keep the cached value live — renderer gets a
    zero-latency stream and the IPC for the same state.

B2. Removed the 1s renderer-side polling loop in workspaceDesktop.tsx
    that re-queried runtime:getStatus while status === "starting".
    The push event already fires the moment the sidecar flips to
    "running"; the poll could only *delay* observed ready by up to
    a full tick (waiting for the next 1s boundary).

Baseline measurement (5.15s total to splash unmount):
  runtime-healthy            3485ms
  first-list-workspaces      5151ms  ← 1.66s gap to fix here

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): hydrate splash from local SQLite — 5.2s → 2.2s

The "Preparing your desktop" splash was gated on the runtime sidecar
spawning + first /api/v1/workspaces query — 3-4s of cold-start work
the user had to stare at on every launch. Three coordinated changes
collapse the wait to under the dev-mode Vite floor (~2.2s; prod will
land closer to ~1s):

1. Decouple workspace load from bootstrap. The workspace-load effect
   used to wait on `isLoadingBootstrap`, which fetches runtimeConfig
   + clientConfig (sidecar IPCs that on cold launch retry against a
   not-yet-healthy sidecar for 1.5s+). None of that bootstrap data is
   needed to render the workspace list. Effect now only gates on
   `runtimeReadyForWorkspaceData`; bootstrap data lands in state in
   the background.

2. Static splash in index.html. Window paints at ~1s but the React
   tree (and its WorkspaceBootstrapPane) used to take another ~450ms
   to mount, leaving a blank window. Inlined matching markup (logo +
   halo + dots animation) so the splash is visible the moment the
   window shows; App.tsx removes the static element via
   useLayoutEffect once React commits — no flash.

3. Optimistic local hydration. Desktop and runtime share runtime.db;
   the desktop main process opens it during bootstrapRuntimeDatabase
   to keep its own workspaces table mirror. New
   listWorkspacesFromLocalDb() reads that mirror directly (read-only
   SQLite handle, ~5-15ms total including IPC), bypassing the sidecar
   entirely. New optimistic-hydration effect on mount fires the cached
   IPC; if any rows come back, splash unmounts immediately. Sidecar
   still spawns + the regular workspace-load effect still runs, doing
   reconciliation in the background. Fresh-install / no-rows case
   falls through to the existing sidecar path, no behaviour change.

Measured (dev mode, cold launch, baseline boot-timing JSON archived):
  before: renderer-hydrated  5151-5984ms (high variance)
  after:  renderer-hydrated  2195ms
  delta:  ~3.6s saved (62%)

Remaining 2.2s is dominated by Vite dev-mode bundle load + React
mount; production builds skip both and should land closer to ~1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): light-mode invisible text from splash CSS body color leak

Splash CSS in index.html set `color: #f5f5f5` on body (chosen via
prefers-color-scheme), which always wins over Tailwind's base
`text-foreground` for elements inheriting from body without an
explicit text-foreground class. When system was dark but app was on
the Holaos light theme, body color stayed near-white → SelectValue
inner span and any other inherited-coloured text went invisible on
white surfaces.

Scope the splash colours to `#boot-splash` itself; let Tailwind's
@apply text-foreground drive body color from the active app theme.

Bonus: SettingsMenuSelectRow forces `data-placeholder:text-foreground`
so the selected value isn't rendered in muted colour while Base UI's
items list is still unregistered (it lazy-mounts inside SelectContent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): drop divider under file identity header in preview

The horizontal line between the file name/meta row and the textarea
content was rendering visibly stronger than intended (and unwanted)
when previewing a file like workspace.yaml. Removed the border-b on
the identity header — content area's own padding already separates
the two sections cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(desktop): drop divider under file identity header in preview"

This reverts commit 0f5715bd7d5fecb3cc632b06de6c00dc81e5a039.

* fix(desktop): tone down brand focus ring + drop boot-timing scaffolding

The global :focus-visible rule painted a 2px brand-coloured outline
plus a 3px box-shadow ring on every input/button/textarea. On full-
pane editors (file preview textarea) the outline rendered as a thick
orange line flush against the surrounding chrome — mistaken for a
divider, hard to ignore.

Switched to a single 2px box-shadow at 50% ring alpha (no outline,
so no offset bleed past overflow boundaries) and removed textarea
from the rule's targets — full-pane editors don't benefit from a
chrome-flush ring, and per-textarea components can opt back in if
they need one.

Also dropped the boot-timing instrumentation that was added during
the splash-speed work — the optimisation has landed, the JSON logs
under userData/boot-timings/ aren't useful day-to-day. Removed:
  - desktop/electron/bootTimer.ts
  - all bootTimer.{start,mark,complete} call sites in main.ts
  - boot:mark / boot:getTimings IPC handlers
  - electronAPI.boot.{mark,getTimings} preload + types
  - 6 renderer-side boot.mark calls in workspaceDesktop.tsx

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): integration account display rework — Phase 1 + Phase 2 Slice 1

Centralizes integration account metadata so handle/email/avatar render
consistently across IntegrationsPane, AppSurfacePane, and AppCatalogCard
without each surface re-fetching whoami on mount.

Phase 1 (frontend cache):
- integrationAccountStore.ts — module-level useSyncExternalStore cache;
  dedupes inflight fetches by account_external_id.
- integrationDisplay.ts — single canonical label precedence chain
  (meta.handle → conn.account_handle → meta.email → conn.account_email
   → meta.displayName → filtered conn.account_label → Account N).
- All three pickers migrated; per-pane local metadata state removed.
- Cache invalidated after dedupe-merge and disconnect.

Phase 2 / Slice 1 (per-provider whoami extraction):
- extractComposioIdentity normalizes Composio whoami across providers
  with explicit handlers for Twitter/X, GitHub, Reddit, LinkedIn, and
  Google variants; generic fallback probes common field names.
- composioFinalize and the legacy-row backfill loop both route through it.
- workspace:composioRefreshConnection IPC re-runs extraction against an
  existing connection's external id and persists newly-resolved fields.
- IntegrationsPane row gains a Refresh button next to Disconnect.

Plan: docs/plans/2026-04-28-integration-account-display-rework.md.
Phase 2 Slice 2 (schema columns + periodic refresh) and Phase 3
(server-side dedupe at finalize) are sequenced as separate work items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): dashboard file type + built-in renderer (workspace shared SQLite)

Design plan for treating dashboards as workspace files (.dashboard YAML)
rendered by a built-in desktop renderer over a workspace-shared SQLite
data plane. Marketplace stays scoped to integration apps; dashboards are
agent-authored artifacts, not separately-published apps.

Architecture:
- Single workspace/<id>/.holaboss/data.db (WAL, lockfile-protected).
- Apps write tables prefixed by app id (twitter_posts, linkedin_posts,
  reddit_posts) via a new @holaboss/bridge.getWorkspaceDb() helper.
- .dashboard panels: kpi (single-value card) + data_view (one query,
  multiple switchable views). v1 views: table + read-only board (Kanban).
- Two runtime-provided MCP tools for the agent: list_data_tables
  (introspects shared db) and create_dashboard (writes the YAML file).

Phased delivery:
1. Shared SQLite plumbing — Twitter pilot.
2. Renderer — kpi + data_view (table+board) views.
3. Agent authoring — list_data_tables + create_dashboard tools.
4. LinkedIn + Reddit migration.
5. Deferred: chart panels, gallery/calendar/timeline views, board drag,
   filters, visual editor.

All six pre-coding open questions confirmed:
- Renderer built into desktop.
- v1 panels = kpi + data_view(table+board).
- Board read-only in v1 (no drag-to-update).
- Files live at files/dashboards/*.dashboard.
- Migration: copy + rename + back up old per-app db.
- Agent schema awareness via list_data_tables tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(bridge): getWorkspaceDb helper for shared workspace SQLite

Adds a new SDK helper so module apps can write/read a single
workspace-shared SQLite file (workspace/<id>/.holaboss/data.db) instead
of each maintaining a private ./data/module.db. Same handle is reused
across calls within a process; the underlying file path is read from
WORKSPACE_DB_PATH (injected by the runtime — wired up in a follow-up).

- env.ts: resolveWorkspaceDbPath() returns the WORKSPACE_DB_PATH env value.
- workspace-db.ts: getWorkspaceDb() opens the file via better-sqlite3,
  enables WAL + foreign keys, creates parent dir, and caches the handle.
  Re-opens transparently if the env path changes between calls.
- better-sqlite3 declared as optional peerDependency so SDK consumers that
  only use the integration proxy don't pay the native-binding install.
- Tests cover env resolution and the missing-env guard. End-to-end exercise
  against a real db file lives in consuming app suites (the better-sqlite3
  native binding doesn't load under bun:test).
- Version bumped to 0.2.0-beta.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): inject WORKSPACE_DB_PATH for apps that opt in via env_contract

When buildShellLifecycleEnv builds an app process's environment, it now
adds WORKSPACE_DB_PATH=<workspace>/.holaboss/data.db whenever the app's
env_contract declares the variable. Apps then call the bridge SDK's
getWorkspaceDb() to read/write the workspace-shared SQLite, replacing
the per-app private ./data/module.db model.

- ts-runner-session-state.ts: workspaceDataDbPath(workspaceDir) helper,
  sibling to workspaceSessionStatePath. Both live under the existing
  `.holaboss` per-workspace folder.
- app-lifecycle-worker.ts: env_contract-gated injection of WORKSPACE_DB_PATH,
  matching the pattern already used for HOLABOSS_USER_ID and
  HOLABOSS_WORKSPACE_ID. Invalid workspace ids (sanitize throws) skip the
  injection silently — the underlying error surfaces elsewhere.
- One-line unit test pins workspaceDataDbPath's path layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bridge): restore syncAppResourceOutput export to match published 0.1.1-beta.1

The bridge source had drifted behind the published 0.1.1-beta.1 npm
artifact, which exports syncAppResourceOutput plus its
AppResourceOutputInput / AppResourceOutputResult types. Module apps
(twitter, linkedin, attio, …) all import these via their local
holaboss-bridge.ts shim, so building from source as-is would produce a
0.2.0 release that regresses every consumer.

Reconstructs the missing surface in source from the published d.ts and
js artifacts:
- AppResourceOutputInput / AppResourceOutputResult in types.ts.
- syncAppResourceOutput in workspace-outputs.ts — three-branch behavior
  (existingOutputId → PATCH, context → session artifact, neither →
  workspace output). No-op envelope when canPublishAppOutputs is false.
- Re-exported from index.ts.

Carries the version bump to 0.2.0-beta.0 introduced in the previous
commit; once both this and getWorkspaceDb land, 0.2.0 is publishable
with no consumer breakage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): install app from any local archive (dev tool)

Adds an "Install from file…" button on the Marketplace → Apps gallery
that lets a developer pick any .tar.gz / .tgz produced by
hola-boss-apps/scripts/build-archive.sh and install it directly,
without dropping the file into hola-boss-apps/dist/ first.

Flow:
1. Click button → Electron file picker opens with a .tar.gz / .tgz
   filter.
2. Main process reads app.runtime.yaml from the archive (system tar
   binary, no new Node tar dep) to resolve the app slug — no need for
   the user to type the app id.
3. If the picked file is outside the runtime's allowlist
   (os.tmpdir() / $HOLABOSS_APP_ARCHIVE_DIR / ~/.holaboss/downloads),
   it's copied into ~/.holaboss/downloads first, then cleaned up after
   install.
4. Same /api/v1/apps/install-archive request as the catalog path; the
   existing app-install-progress IPC events fire so the gallery state
   updates uniformly.

Cancelling the file picker resolves to null so the renderer can
distinguish cancel from error and avoid spurious refresh calls.

Errors (validation, manifest peek, install) surface inline above the
gallery instead of throwing — picks an app id from `slug` (preferred)
or `app_id` and rejects archives that declare neither.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): wire Remove app menu item via onClick (was a Radix-style onSelect)

The dropdown menu component is built on @base-ui/react/menu, where the
click handler is onClick — the same pattern every other DropdownMenuItem
in the codebase uses (AuthPanel, AutomationsPane, IntegrationsPane,
TopTabsBar, etc.). AppSurfacePane was the only place still using a
Radix-style onSelect callback, so the handler never fired and clicking
"Remove app" did nothing — the confirmation bar never appeared, the
DELETE never went out.

Drops the event.preventDefault() too — that was guarding against Radix
auto-closing the menu on select, but here we want the menu to close
once the destructive confirm bar opens, so closing is the right
behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bridge): switch getWorkspaceDb to static better-sqlite3 import

The previous createRequire(import.meta.url) pattern broke when consumers
re-bundled the SDK's ESM source into a CJS output (which is exactly
what the module-app build does — esbuild bundles start-services.cjs
with --format=cjs and pulls bridge's ESM entry, leaving import.meta.url
undefined at runtime). The MCP services process crashed at boot with
ERR_INVALID_ARG_VALUE before the workspace data.db was ever opened, so
the install-archive flow couldn't get the app to a healthy state.

Switching to a static `import Database from "better-sqlite3"` works in
both the SDK's own ESM/CJS dist and inside any downstream bundle —
better-sqlite3 stays a peer dependency, marked external by both
tsdown (via dependencies/peerDeps) and esbuild (via --external on the
consumer side), so the native binding still resolves through the
consumer's installed copy.

Drops the loadBetterSqliteSync wrapper now that the import is static.

Bumps bridge to 0.2.0-beta.1 — apps that pinned 0.2.0-beta.0 need to
update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): dashboard renderer for .dashboard YAML files (Phase 2)

Built-in renderer for the Notion-style structured dashboard format.
Picks up the workspace-shared SQLite from Phase 1 — every panel runs
its own SQL against workspace/<id>/.holaboss/data.db (read-only) and
renders into one of two panel kinds.

Architecture:
- main.ts runDashboardQuery — short-lived read-only better-sqlite3
  handle per call (PRAGMA query_only=ON). Caps result at 5k rows.
  Resolves the workspace data.db path through the existing
  resolveWorkspaceDir helper.
- IPC dashboard:runQuery returns { ok, columns, rows } | { ok, error }
  so a panel-level SQL failure surfaces inline without crashing the
  whole renderer.
- dashboardSchema.ts — hand-written YAML parser + validator (no zod
  dep). Supports kpi panels and data_view panels with table + board
  views. Each parse error names the offending panel/view index.

Components (all under components/dashboard/):
- KpiCard — single-value card. Convention: prefer column named `value`,
  fall back to first column of first row.
- TableView — column whitelist + order, paged at 500 visible rows.
- BoardView — read-only Kanban grouped by group_by; cards show
  card_title plus optional card_subtitle; per-column row cap of 200.
- DataViewPanel — owns active-view state, renders a tab bar when the
  panel has ≥ 2 views, dispatches to TableView or BoardView.
- DashboardRenderer — parses, runs all panel queries on mount/refresh,
  renders KpiCard or DataViewPanel per panel slot.

File integration:
- .dashboard added to TEXT_FILE_EXTENSIONS so readFilePreview returns
  YAML content as a text preview.
- FileExplorerPane: when a previewed text file's extension is
  .dashboard and a workspace is selected, render <DashboardRenderer>
  in place of the editor/markdown surface.
- File-icon mapping added so .dashboard shows a database glyph in the
  tree.

Smoke-test workflow:
- Hand-craft a `.dashboard` file under workspace/<id>/files/dashboards/.
- Click into it from the Files pane → KPI cards + DataView panel
  with table/board tabs render against twitter_posts.

What's deferred to Phase 3:
- Agent system tools list_data_tables and create_dashboard (so the
  agent can author dashboards from chat).
- Chart panels (line/bar/pie), gallery view, calendar/timeline,
  drag-to-update on the board, filter parameters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): wire dashboard renderer into InternalSurfacePane too

FileExplorerPane has a sibling preview surface, InternalSurfacePane,
that takes over for full-pane file views (the case actually exercised
when opening a .dashboard file from the workspace tree). I'd only
patched the FileExplorerPane branch, so the dashboard YAML kept
rendering through InternalSurfacePane's textarea fallback.

Adds the same `extension === ".dashboard"` short-circuit before the
textarea, importing the same DashboardRenderer. selectedWorkspaceId
is already in scope here via useWorkspaceSelection so no plumbing
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): retune dashboard renderer to Notion aesthetic

The cards-with-shadow + colored-header chrome from the first cut felt
heavy next to the rest of the workspace. Pass over each component to
shed surface weight: flat surfaces, subtle borders, generous whitespace,
text-led tab bar.

DashboardRenderer
- Drop the bg-muted/30 outer wrap and bg-background/60 sticky header.
  Page is plain bg-background with the title/description block sitting
  flush at the top of a centered 3xl column.
- Refresh demoted to a muted-foreground ghost button on the right.
- Consecutive kpi panels collapse into one CSS-grid row (up to 4 wide)
  so a Total/Published pair reads as a stat strip instead of two
  stacked cards.
- Larger inter-panel gap (gap-10) for breathing room.

KpiCard
- Strips border, shadow, and card background entirely. Just an
  uppercase muted label above a 3xl tabular-nums number.

DataViewPanel
- No surrounding card chrome. Title sits inline above a hairline
  border-b; tab bar lives in that same row, right-aligned.
- Tabs are now underlined text (border-b-2 on active) instead of pill
  toggles inside a bg-muted track.

TableView
- Removes zebra stripe (even:bg-muted/30). Headers gain an uppercase
  muted-foreground treatment; rows separated by a soft border-b/60
  hairline. Hover state is a single bg-muted highlight.
- Drops the per-cell px-3 padding in favor of py-2 pr-4 with a
  first:pl-0 reset so the table aligns flush with the title above it.

BoardView
- Columns are now plain stacks of cards — no surrounding bg-muted/40
  card with border. Just the title + count, then cards.
- Cards lose their border + shadow; bg-muted is enough to delineate.
  Hover swaps to bg-accent for affordance.
- Wider inter-column gap (gap-5) so the board breathes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): icon-button view switcher + roomier table rows

DataViewPanel
- Replaces the underlined-text Table/Board tabs with two square
  icon buttons (Table2 + Columns3 from lucide). Each button is
  size-7, rounded-md, with bg-muted active state and a hover swap —
  same shape language as the More-options button on the app surface
  toolbar so toolbars stay consistent across the desktop.
- title + aria-label keep the labels accessible without showing them
  on screen.

TableView
- Bumps row text from text-xs to text-sm and increases vertical
  padding (py-2.5) so the table reads less dense.
- Adds px-3 horizontal cell padding with first:pl-1 last:pr-1 resets
  so the table aligns flush with the panel's title above it.
- Soft hairline rows (border-b/40) with the last row left
  borderless — looks closer to Notion's table than the previous
  uniform hairline.
- Hover state lifted slightly to bg-muted/70 instead of bg-muted so
  the change is visible against an already-flat surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): list_data_tables + create_dashboard agent tools (Phase 3)

Two new system-level capability tools expose the dashboard authoring
loop to the agent: discover what's queryable in workspace data.db,
then write a `.dashboard` file that the renderer (Phase 2) picks up.

list_data_tables (GET /api/v1/capabilities/runtime-tools/data-tables)
- Opens the workspace's shared data.db read-only (PRAGMA query_only),
  walks sqlite_master + PRAGMA table_info for every non-internal
  table, and returns { tables: [{ name, columns, row_count }] }.
- Returns empty list with a note when data.db doesn't exist yet (no
  module app has written rows).

create_dashboard (POST /api/v1/capabilities/runtime-tools/dashboards)
- Validates the spec: title required, at least one panel, each panel
  is `kpi` (single-value) or `data_view` (with ≥1 view, table + board
  shapes), data_view's `default_view` enum-matches.
- Dry-runs every panel's SQL against data.db with `LIMIT 0` so a
  parse / column / table mistake surfaces immediately as a 400 with
  a typed error code, instead of silently writing a broken file.
- Sanitizes the slug ([a-z0-9._-]+, ≤80 chars), then writes
  workspace/<id>/files/dashboards/<name>.dashboard.

Tool registration plumbing
- harnesses/runtime-agent-tools.ts: id + description + policy entries.
- harnesses/runtime-capability-tools.ts: JSON-schema for tool params
  used by the agent's tool surface.
- harnesses/runtime-tool-capability-client.ts: maps tool ids to HTTP
  request plans.
- api-server/runtime-agent-tools.ts: wires the path/method into
  RUNTIME_AGENT_TOOL_DEFINITIONS, adds RuntimeAgentToolsService
  methods, plus helpers (sanitizeDashboardFileName,
  validatePanelInput, serializePanel).
- api-server/app.ts: GET + POST routes that delegate to the service.

Test coverage
- runtime-agent-tools.test.ts: 5 cases — empty data.db, populated
  introspection, end-to-end dashboard write + YAML readback,
  bad-SQL rejection (no file written), unsafe-slug rejection.

Also rolled in: a small linter pass on the dashboard TableView.tsx
that normalized text-[11px] → text-xs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): badges for status fields, segmented view switcher, card chrome on data_view, clamped long cells

Four refinements based on UX feedback while testing dashboards.

1. View switcher → segmented button group
   The Table/Board icons now live inside a single bordered track
   (border + bg-muted/40 + p-0.5) and the active button stands proud
   with bg-background + shadow-xs, mirroring the segmented-control
   shape used elsewhere in the desktop. Buttons themselves are
   slightly smaller (size-6 vs size-7) so the pair reads as a single
   compact control rather than two loose icons.

2. Status fields render as badges
   New StatusBadge component + name-based heuristic
   (`isStatusColumn`) — columns named status / state / stage / phase
   / lifecycle hand the cell value to a small pill colored by token:
   - published / active / completed / sent / done → bg-success/10
   - failed / error / cancelled / rejected → bg-destructive/10
   - queued / pending / processing / running → bg-info/10
   - scheduled / delayed / waiting / paused → bg-warning/10
   - draft / new / idle / open → bg-muted (neutral)
   - unknown values fall back to the muted neutral, never a wrong
     semantic color. BoardView's group-by header swaps the plain
     `<span>` for the same StatusBadge when the group_by column is
     status-like, so column names like "draft" / "published" /
     "failed" pick up matching tints.

3. data_view panel gets card chrome
   Each data_view now lives inside a rounded-lg bordered card with a
   header strip (title + row count + view switcher) above a
   border-b hairline, body sits below with px-4 pb-3 padding. KPI
   panels stay flat as before — their density is the inline-stat
   value, not card-level.

4. Long cells get a 2-line clamp + native tooltip
   Cells with content longer than 80 chars get line-clamp-2 with the
   full text as the `title` attribute, so a wall of tweet-length
   prose stops blowing up the row height. Status columns are
   exempted (always one line). Table is now table-fixed with a
   140px col for status columns so the badges line up neatly and
   text columns share the remaining width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): full-width toggle in dashboard header (Notion-style)

Default reading width bumps from max-w-3xl (768px) to max-w-4xl
(896px) so two-column tables and KPI rows breathe a bit more without
giving up the centered-document feel.

For wider data, a Notion-style toggle now sits left of the Refresh
button. Click flips the page between max-w-4xl and max-w-none
(genuine full width). State persists in localStorage under
`dashboardRenderer:fullWidth` so the chosen width sticks across
sessions per browser profile.

Icon swaps based on state — Maximize2 when compact, Minimize2 when
full width — with a corresponding label so the affordance reads
without needing the tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): lift dashboard toolbar to outer pane + icon-only with tooltips

The full-width and refresh buttons no longer share the centered
content's max-width — they now live in a sticky top-right strip
inside the pane's outer scroll container, anchored above the page
title regardless of the chosen page width. Switching to full width
no longer drags the controls across the screen.

Buttons are icon-only (size icon-sm) and use the existing Base UI
Tooltip component instead of native title attributes — tooltip
"Full width" / "Compact width" / "Refresh" appears on hover with
the bottom side aligned, matching how other icon-only toolbars in
the desktop are wired.

The strip uses a soft bg-background/85 + backdrop-blur so any
content scrolled under it stays legible without a hard divider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): move dashboard toolbar into the file preview header

The previous sticky strip sat inside the renderer body, which meant
two top-bars stacking on top of each other when a .dashboard file was
open: the file metadata header (filename, size, modified date) plus
the renderer's own controls. Lifting the controls into the file
header collapses both into one strip, the way Eye/Save already share
that surface for markdown / editable files.

InternalSurfacePane now owns:
- dashboardFullWidth (persisted to localStorage as before)
- dashboardRefreshKey (bumped on every Refresh click)
- Two icon-only Tooltip buttons rendered next to Eye/Save when the
  open file's extension is .dashboard.

DashboardRenderer no longer manages either piece of state — it
accepts `fullWidth` and `refreshKey` as props and renders the body
only. The host pane drives state changes via the lifted handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): post metrics convention — Twitter first, cross-platform shape

Lays out the schema convention every platform pack will follow:
<platform>_post_metrics (snapshot, PK post_id+captured_at),
<platform>_post_metrics_daily (rollup), <platform>_metrics_runs
(scheduler activity log), <platform>_api_usage (passive call counts).
Common columns impressions/likes/comments/shares mean the same thing
across platforms; platform-specific columns (bookmarks, unique_views)
allowed alongside.

Refresh model: app-internal scheduler (Option C from the design
discussion). The 5-min interval lives in twitter's start-services.ts
and calls a pure refreshPostMetrics() function — same function the
twitter_refresh_post_metrics MCP tool wraps for agent-driven manual
refresh. Workspace cron is left out of this loop because it routes
through the agent (LLM call per fire) which would burn ~$10/day in
cron-translation cost. Revisit when ≥ 3 apps need centralized
scheduling.

Tier policy lives in code (5min/30min/6h/1d/frozen by post age) so a
single cron tick can drive the whole schedule and tier transitions.

Backfill bound: only refresh posts < 7 days old at first launch;
older posts enter frozen tier. Agent can force-refresh via
{ post_ids, force: true }.

Pause via workspace.yaml `apps.twitter.metrics_refresh: enabled`,
plus a twitter_set_metrics_refresh MCP tool for chat-driven toggles.

Phased: M1 (this doc + schema) → M2 (refresh tool + tier logic) →
M3 (scheduler + backfill bound + pause flag) → M4 (daily rollup +
90d retention) → M5 (sample dashboard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dashboard): smooth full-width transition + better icons + hide app-internal tables from agent

Two issues from the metrics test pass.

1. Full-width toggle felt janky and the icons weren't right
- max-w-none animates over a literal-billion-px range under
  transition-[max-width], so the swap visibly didn't ease — replaced
  with a concrete max-w-[1600px] for the wide state.
- Added transition-[max-width] duration-200 ease-out on the centered
  content container so width changes glide instead of snap.
- Icons swapped from Maximize2 / Minimize2 (square diagonal arrows
  that read as "expand the whole window") to ChevronsLeftRight /
  ChevronsRightLeft (horizontal-axis arrows that read as "expand
  width" / "contract width"). Tooltip labels unchanged.
- Active state on the button now uses bg-muted text-foreground when
  full-width is on — the toggle itself reads as a press state, not
  just an icon swap, so the user sees the click took effect.

2. App-internal tables shouldn't show up in the agent's "what can I
   query?" view
- runtime list_data_tables now filters out tables whose name ends in
  one of the convention's app-internal suffixes (_jobs / _metrics_runs
  / _api_usage / _settings / _migrations). These are operational
  state for the apps themselves, not user-facing data — agents
  composing dashboards almost never need them and their visibility
  just adds noise to the schema list.
- Tool gains an optional include_system parameter (default false)
  for the rare case the agent really does want them. Response
  surfaces a hidden_system_count + a one-line note pointing at the
  override so the agent can re-query if it determines it needs them.
- Tool description rewritten to mention the filter; runtime client +
  capability schema updated to forward the new parameter.

Test coverage: new runtime-agent-tools.test.ts case seeds the four
internal table shapes alongside twitter_posts and twitter_post_metrics,
asserts the default response shows two tables with hidden_system_count
4, and asserts includeSystem=true shows all six.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): drop Tooltip wrapper on dashboard toolbar buttons

The Refresh + Full-width buttons were silently swallowing clicks under
the <TooltipTrigger render={<Button onClick=...>}> pattern in this
toolbar. Eye + Save in the same toolbar use plain <Button title="...">
and work, so collapse the dashboard pair to the same shape — drop
TooltipProvider / Tooltip / TooltipTrigger / TooltipContent in favor
of native title= tooltips.

Functionally identical user-facing affordance (hover shows the label),
none of the click-routing surprises that came from layering Base UI's
useRender on top of Button (which is itself a useRender wrapper around
ButtonPrimitive).

aria-pressed / aria-label preserved so accessibility doesn't regress.
The active-state visual feedback (bg-muted on full-width toggle)
stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* debug(desktop): log dashboard toolbar state changes (TEMP — to be reverted)

* fix(desktop): share dashboard toolbar state across InternalSurfacePane instances

Two InternalSurfacePane instances mount simultaneously (chat surface
+ main display) when a .dashboard file is open. With useState, each
held its own dashboardFullWidth value; clicking the visible button
flipped one instance's state while the other kept rendering its stale
view, so the user saw the click "do nothing." Console logs confirmed
the divergence: clicked-instance's `before` value stayed pinned at
true while the other instance's effect log alternated.

Replaces the per-instance useState with a module-level store
(src/lib/dashboardToolbarStore.ts) read via useSyncExternalStore.
fullWidth and refreshKey now have one source of truth, every mounted
pane subscribes, and a single click updates every instance at once.
The localStorage persistence for the width preference moves into the
store too.

Click handlers on the toolbar buttons no longer wrap useCallback
locals — they call the store's exported toggleDashboardFullWidth /
bumpDashboardRefreshKey directly, so the JSX has no stale-closure
foothold.

Drops the temporary debug console.logs from the previous diagnostic
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): dedicated dashboard icon + label for .dashboard artifact cards

The agent-authored .dashboard files showed up in chat artifact cards
with the generic file-document treatment (gray tile + plain page
icon + "File · 3 KB · 5:08 AM" secondary text). Now they get a
first-class visual:

- New `dashboard` OutputVisualKind, branched off the same extension
  detection path that already handles spreadsheet / pdf / code /
  document so an output's `.dashboard` title is enough — no
  metadata category required.
- LayoutDashboard from lucide as the icon (the four-quadrant glyph
  reads as "dashboard" universally), tinted to the brand primary
  to match the app artifact treatment — they're peers in
  importance: app surfaces and dashboards are the two
  agent-authorable surface types in the workspace.
- outputKindLabel returns "Dashboard" for these files so the
  secondary line reads "Dashboard · 3 KB · 5:08 AM" instead of
  "File · …" — small but the cards now communicate what they are
  at a glance.
- Helper outputFileExtensionFromTitle factored out because
  outputKindLabel needs the extension before the metadata-aware
  outputFileExtension is called. Falls back to file_path metadata
  when title doesn't carry a suffix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): refresh integration connections on Reload + window focus

Same fix as a4dc9cf on feat/multi-account-integrations, ported to the
toolbar layout introduced by 68f89af.

The "Connect my account" CTA only fetched the connection list once on
mount, so users who reconnected an account in Settings and came back to
the app pane saw a stale empty list. The toolbar Refresh button only
swapped the iframe key — checkIntegration never ran again.

- handleRetry refetches integrations in parallel with refreshInstalledApps.
- onRetry's ready branch refetches integrations alongside the iframe reload.
- Adds a window-focus + visibilitychange listener (3s throttled) so the
  common OAuth path (Holaboss → browser → back) picks up the new
  connection automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): remount AppSurfacePane on appId change

AppSurfacePane carried local state (wasRemoved, frameUrl, frameLoading,
frameError, integrationContext) across appId changes because the two
render sites in AppShell didn't pass a key. Switching from one app to
another reused the same fiber, so stale state from the previous app
could leave the new app's pane visually empty until the user navigated
away (forcing an unmount) and back.

Adding key={appId} forces a fresh mount per app, matching the workflow
that already worked (switch to browser/files and back). Iframe reload
behavior is unchanged — the iframe was already keyed on frameUrl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): app surface toolbar uses AppIcon resolver instead of local-only providerIcon

The toolbar chip was calling providerIcon(appId, 22) directly, which
only knows about a hardcoded set of brand SVGs — anything outside that
list (e.g. Attio) silently fell through to a 2-letter initial chip even
though the sidebar's AppIcon would have found the logo on the Composio
CDN.

Adds a "toolbar" size variant to AppIcon (28px chip / 22px logo,
matching the existing toolbar styling) and points the AppSurfacePane
toolbar at it. The full backend → CDN → local-svg → letter fallback
chain now applies here too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): artifacts browser slides in from right with inbox-style cards

The "View all artifacts" overlay was a centered modal with single-line
button rows. Replaced with a right-edge slide-in pane (absolute inset-y-0
right-0, w-[360px], slide-in-from-right-4) that mirrors the Inbox shape:
ArrowLeft "back to chat" header, compact bordered card rows with an
uppercase kind eyebrow above the artifact title.

Behavior is unchanged from the user's POV — artifactBrowserOpen +
artifactBrowserFilter state still drives it, click still calls
onOpenOutput. Just the surface is now a side pane, not a centered modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): artifacts pane full-bleed; inbox + artifacts share slide-in animation

Two adjustments to the prior artifacts redesign:

1. Artifacts pane was a 360px right-anchored sliver; now fills the
   chat pane (absolute inset-0) so it visually mirrors how Inbox takes
   over the agent column.
2. Inbox was a hard React swap with no entry animation. Now both
   surfaces share animate-in fade-in-0 slide-in-from-right-3
   duration-200 ease-out, so opening either feels like the same
   side-pane gesture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): apply side-pane slide-in animation to all agentContent views

Extends the animate-in fade-in-0 slide-in-from-right-3 duration-200
ease-out treatment from inbox to the remaining agent-column views:
automations, app surface (AppSurfacePane), and internal surface
(InternalSurfacePane). AppSurfacePane and InternalSurfacePane don't
own their outer wrapper, so wrap them in a flex shell that owns the
animation.

Now any agentView.type switch into a non-chat surface lands with the
same gesture, matching what the user expects from the inbox flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): merge composer Pause pill into the send button slot

Two side-by-side buttons (Pause pill + ArrowUp send) felt redundant
during a response. Replaced with a single icon-sm slot that swaps
between Pause (Square / Loader2) when responding and Send (ArrowUp)
when idle. Both share the same rounded-lg footprint so the toggle
reads as one stable control.

Queue-while-responding via Enter in the textarea is unchanged — only
the visible button consolidated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): extend AppSurfacePane knownProviders fallback

The fallback map was stuck at the original 6 apps, so newly shipped
modules (calcom, attio, hubspot, apollo, instantly, zoominfo) had no
provider lookup until a binding already existed. Without it,
checkIntegration() set integrationContext=null and the surface's
"Connect <Provider>" chip didn't render at all — making it impossible
to bootstrap the first binding from the app surface itself.

attio appeared to work only because the user had already bound it via
the integrations pane; calcom had never been connected and so was
silently ungrabbable from its app surface.

* fix(desktop): cal.com toolkit slug is "cal" not "calcom"

Composio's catalog publishes the Cal.com toolkit under slug "cal" (the
card in the integrations pane is titled "Cal"). The provider-related
maps were previously keyed under "calcom" so they never matched the
real toolkit; harmless until we needed AppSurfacePane to render the
"Connect Cal.com" chip for the calcom app whose binding hadn't been
created yet.

Fixes:
- AppSurfacePane.knownProviders maps app id "calcom" → toolkit slug "cal"
- IntegrationsPane.PROVIDER_CATEGORY_GROUPS / TOOLKIT_PREFERENCE keyed
  on "cal", matching what providerIdForToolkit returns at runtime

* fix(desktop): drop fixed pixel widths on composer compact controls

The compact-mode wrappers were forcing the model picker and thinking
selector to specific pixel widths (compactModelControlWidth /
compactThinkingControlWidth). With short labels like "GPT-5.4" the
ModelCombobox trigger's w-full justify-between then pushed the chevron
to the far right of the wrapper, leaving a wide blank gap between the
label and the dropdown caret.

Both wrappers now just shrink-0 / fit content. The ThinkingValueSelect
still receives compactWidth so its icon-only branch keeps firing in
compact mode — only the outer width clamp goes away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): tighten chat composer textarea vertical padding

The textarea wrapper had 16/12 top/bottom padding (pt-4 pb-3) which left
visible dead space above the placeholder and a noticeable gap before the
footer. pt-3 pb-2 (12/8) reduces 8px of vertical air without crowding
the input cursor or hiding the focus state.

Horizontal asymmetry (px-5 textarea vs px-3 footer) is intentional —
keeps the placeholder caret column-aligned with the model picker brand
icon at the bottom — so left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
…olaboss-ai#228)

* chore: gitignore website/docs build outputs

The docs-next site (TanStack Start + Fumadocs) leaves .react-router/,
.source/, and build/ in website/docs/ on every build, which keeps
nagging in git status. Match the existing pattern used for the
.vitepress cache/dist directories.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(integrations): user-global connections + reverse migration + owner_user_id check

Connections are user-global; per-workspace scoping lives entirely in
integration_bindings. The short-lived feat/composio-workspace-scoped-accounts
branch added a workspace_id column to integration_connections that conflicts
with the "one account → many workspaces" requirement.

- state-store: add migrateRevertIntegrationConnectionsWorkspace, which
  detects the legacy column, materializes each non-null workspace_id row
  into a default workspace binding (skipping rows that already have one),
  drops the now-stale workspace-provider index, and ALTER TABLE DROP
  COLUMN workspace_id (SQLite ≥ 3.35).
- state-store: regression tests covering legacy-with-data upgrade
  (3 mixed scenarios) and fresh-DB no-op.
- api-server: add resolveOwnerUserId helper that, when HOLABOSS_USER_ID is
  set (managed/sandbox), rejects bodies whose owner_user_id mismatches.
  In OSS local mode (env unset) behaviour is unchanged.
- api-server: apply ownerCheck to POST /api/v1/integrations/connections
  and /api/v1/integrations/composio/finalize.
- api-server: composio/finalize now accepts an optional workspace_id and,
  when present, atomically upserts a default workspace binding alongside
  the new connection — the connection itself remains user-global.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-account Settings → Integrations + per-app binding selector

- Settings → Integrations now lists every active connection per provider
  (no provider-level dedup), each as its own row with its own disconnect
  button. Replaces the old "one Connected pill per provider" UX that
  silently hid additional accounts. The Connect button on a connected
  card now reads "Connect another <provider> account".
- AppSurfacePane's read-only "Connected/Not connected" pill becomes a
  Select dropdown of every active account for the app's expected
  provider. Picking one upserts an app-level integration_binding so this
  workspace's copy of the app uses that account; switching workspaces
  or apps preserves each binding independently. The dropdown's "Connect
  new account" entry opens Settings → Integrations.
- Empty state when the user has no accounts for the provider: a Connect
  CTA that opens Settings → Integrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): tighter Settings → Integrations card + use Composio profile metadata

The previous ConnectedProviderCard was loud — provider header with logo
+ name + "N accounts" subtitle, each account in its own bordered box
with a green status dot, plus a full-width "Connect another" footer
button. Compress to:

- Header line: small (size-5) provider logo + name + an icon-sized
  Connect-another button on the right. Drop the redundant account count.
- Account row: size-3.5 avatar + handle + disconnect icon, no border or
  status dot. The row's mere presence implies "connected".

Account label/avatar now come from Composio profile metadata:
- Renderer fetches composioAccountStatus(account_external_id) for each
  visible connection in parallel after loadData and stores results in a
  Map<connectionId, ComposioAccountStatus>. Append-only across loads so
  transient fetch failures don't blank avatars already on screen.
- Display priority: handle (prefixed @ if missing) → email → displayName
  → account_label (skipping the boilerplate "(Managed)" suffix) →
  account_external_id → connection_id.
- ComposioAccountStatus type extended in both desktop/electron/main.ts
  and desktop/src/types/electron.d.ts with optional handle, displayName,
  avatarUrl, email, and the raw data passthrough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): drop avatars and Composio-id fallback from account rows

Avatars only render reliably for some providers (Twitter has them,
Reddit and LinkedIn often don't), and the empty grey placeholder noise
isn't worth keeping just for the providers that do. Drop the avatar
column entirely.

When metadata is missing the previous fallback chain ended at
account_external_id — but for managed Composio accounts that's the
"ca_xxx" connected_account_id, which tells the user nothing. Skip any
label that starts with "ca_" and fall through to a stable "Account N"
index instead. The metadata fetch still upgrades the label to a real
@handle / email / displayName once Hono /composio/account/:id starts
returning the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): bring back size-3.5 account avatars

Now that Composio whoami delivers reliable avatar URLs across all
managed providers (Gmail, GitHub, Twitter, LinkedIn, Reddit), put the
avatar back on each account row. When the URL hasn't loaded yet (or a
new provider lacks one), fall through to a tiny lettered placeholder
keyed off the display label's first character — matches the avatar's
size-3.5 rounded-full footprint so the row layout stays stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): cascade delete connection bindings + Google avatar referrer

Two bugs from real-world testing of Settings → Integrations:

1. Disconnect button did nothing for any account that was already used
   in a workspace. The runtime's deleteConnection threw 409 ("connection
   is bound to N workspaces — remove bindings first") and the desktop
   set a barely-visible status message. But the user-global model is
   "one account → many workspaces"; deleting an account is the explicit
   way to detach it from all workspaces, not an error condition.
   Cascade-delete every binding (orphan + live) pointing at the
   connection before dropping the row, and report removed_bindings on
   the response so the UI can mention it later if we want a confirm.
   Updated the test that asserted the old 409 path.

2. Google profile avatars (lh3.googleusercontent.com) rendered as broken
   image icons in Electron. The CDN rejects requests whose Referer
   header includes the renderer's localhost dev origin (and even some
   prod origins for hotlink protection). Setting referrerPolicy to
   "no-referrer" on the avatar img drops the header — provider CDNs are
   uniformly happy with no referrer and the image loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): degrade to letter placeholder when avatar URL fails to load

Provider CDNs can be flaky (Twitter rate-limits, LinkedIn auth quirks),
which today shows up as a broken-image icon next to the row. Track
failed-to-load URLs in component-local state and swap to the same
size-3.5 lettered placeholder we already use when avatarUrl is missing
entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): provider logos missing for Gmail/Sheets/Twitter cards

Two stacked bugs in mergeIntegrationCards left integration.logo null
for several connected providers — the screenshot showed grey letter
placeholders instead of the brand mark for Gmail, Google Sheets, and
Twitter / X:

1. TOOLKIT_SLUG_TO_PROVIDER collapses gmail and googlesheets onto
   provider key "google", so the catalog entries with provider_id
   "gmail" / "googlesheets" miss the toolkitByProvider lookup.

2. Hono /composio/toolkits filters out toolkits whose
   composio_managed_auth_schemes is empty unless the user's namespace
   has a custom auth_config for them. Twitter currently has empty
   managed schemes Composio-side, so a user without a custom Twitter
   auth_config never sees the toolkit reach mergeIntegrationCards.

Both root causes are fragile. Sidestep by falling back to Composio's
public logo CDN — `https://logos.composio.dev/api/{slug}` is a stable
public URL keyed by the toolkit slug, no auth required, returns SVG
for every supported provider including Gmail/Sheets/Twitter. Wired in
when toolkit?.logo is null so we still prefer Composio's curated
toolkit logo when present.

Also added the same referrerPolicy="no-referrer" + onError → letter
fallback we use for account avatars, so a logo CDN hiccup degrades
gracefully instead of showing a broken-image icon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): UI polish for SettingsDialog + TopTabsBar

- SettingsDialog: arbitrary Tailwind values normalized to scale
  utilities (min-w-[140px] → min-w-35, etc.), Select triggers gain a
  visible border instead of border-transparent, break-words → wrap-
  break-word, plus a few formatter reflows.
- TopTabsBar: user dropdown menu icons match the rest of the app's
  icon-in-menu convention (opacity-60 size-3.5), and useDesktopBilling
  destructure reformatted for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plans): manifest-driven integration routing plan (revised)

Revises the original "yaml-driven" draft after design review:

- Source of truth flips from `app.runtime.yaml` to `marketplace.json`,
  with cross-source consistency enforced by sync endpoint + CI lint
  (closes the silent-drift class this design exists to eliminate).
- D1 / D2 / D3 decisions are surfaced as a "Decisions required BEFORE
  Phase 1" section instead of being scattered through Risks.
- New Phase 0 — Composio byo-creds spike — gates Phase 1 schema. Spike
  script lives in `hola-boss-apps/scripts/composio-spike-byo-creds.ts`
  (separate commit in that repo).
- Phase 2 splits into 2A (helper + dual-source fallback, no entries
  removed) and 2B (per-app cutover with reconciler surfacing badges as
  each app flips). Reconciler ships as no-op code in 2A, becomes
  load-bearing in 2B per-app.
- Phase 3 prerequisites tightened: `<CredentialsModal>` UX wireframe
  added (error display, multi-account, post-connect verify, a11y).
- SDK regen step in Phase 1 explicitly notes both desktop AND
  `frontend/apps/web` consume the regenerated `@holaboss/app-sdk`.
- `_template` integration switched from "comment block in README" to
  "extend `create-hola-app` to emit `marketplace.entry.json` into the
  new app's directory" — comments rot, CLI artifacts don't.
- "No PR to holaOS necessary" claim narrowed to "no PR under the
  existing auth modes" (new auth_mode obviously requires holaOS code).
- File name kept (`yaml-driven-...`) to preserve any external
  references; H1 reads "manifest-driven integration routing".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "docs(plans): manifest-driven integration routing plan (revised)"

This reverts commit f89ad79afd4b709ec2e96d7d233a77c724e80ba5.

* fix(desktop): unblock Composio connect and unify app icons

The integration connect flow had three layered failures that compounded
into the reviewer-blocking "Connected account not found" loop:

1. composioFetch hard-failed up front when the Better Auth cookie was
   missing/invalid (throwing "Not authenticated"), bypassing the 401
   re-auth retry that requestControlPlaneJson already had. Extracted a
   shared retryAfterSessionAuth helper (single-flight; coalesces parallel
   401s into one sign-in browser window) and routed both call sites
   through it.

2. Node fetch (undici) reused half-closed keep-alive sockets to
   api.imerchstaging.com, surfacing as ECONNRESET right when the polling
   loop hit the staging endpoint hardest. Added fetchWithNetworkRetry
   that catches ECONNRESET / ETIMEDOUT / SocketError on the first attempt
   and retries once after a 200ms backoff with a fresh connection.

3. The polling loop blindly trusted the connected_account_id returned
   by /api/composio/connect — but that id is not queryable via Composio
   v3's /connected_accounts/{id} endpoint until well after OAuth
   completes (and in some cases never resolves). Replaced the by-id poll
   with a snapshot-based approach: capture the user's current connection
   ids before /connect, then poll /api/composio/connections (a stable v3
   endpoint) for any *new* id matching the toolkit. Applied across all
   four call sites (IntegrationsPane, MarketplacePane, FirstWorkspacePane,
   workspaceDesktop). Added a composioListConnections IPC + preload
   binding to support the new flow.

Side fixes bundled because they came up while debugging the same flow:

- composioListToolkits no longer silently returns an empty toolkit list
  on missing cookie (the prior behaviour masked auth issues as "no
  integrations available")
- New AppIcon component centralises the icon resolver as a 4-stage
  fallback: backend manifest URL → Composio CDN → hardcoded SVG kit →
  letter chip. CDN is preferred over local SVGs because its flat brand
  marks render at uniform sizes, eliminating the visual drift between
  cartoon-style Reddit/Octocat-style GitHub and HubSpot-style letter
  placeholder. Adopted in AppCatalogCard and SpaceApplicationsExplorerPane
- Provider display names extended to cover hubspot/attio/calcom/apollo/
  instantly/zoominfo
- Session search dropdown gets 2px gap between rows for breathing room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): track unavailable MCP servers in pi harness

Surface MCP server unavailability through the harness host so the
desktop and api-server tier can render warnings (vs. silently dropping
tools when an MCP backend is down). Adds:

- PiMcpServerUnavailableInfo type carrying { serverId, reason,
  missingToolIds } per failed server
- unavailableServers field on PiMcpToolset and PiSessionHandle so the
  toolset builder can hand a structured summary back to callers
- mcp_server_unavailable event type in both KnownRunnerEventType
  (harness-host contract) and TsRunnerEventType (api-server contract)
  so consumers can listen for these mid-run

No behaviour change for runs where every MCP server starts cleanly —
unavailableServers stays empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): dedupe re-auth flows + install-time account selector

Two bugs reported on PR #218 once multi-account landed:

  1. Same external account connected twice shows up as two rows. Composio
     re-auth mints a new connected_account_id even for the same real
     account, and the upsert was keyed on connection_id only — every
     re-auth = fresh row. Pre-fix legacy rows had no provider-side
     identity persisted, so they were invisible to any retroactive
     dedupe attempt either.

  2. No way to choose which account to bind when installing an app.
     installAppFromCatalog auto-picked the first active connection for
     the app's expected provider; multi-account users had to install
     first, then go to AppSurfacePane to switch.

Phase 1 — dedupe-on-finalize
- state-store: integration_connections gets account_handle + account_email
  columns (additive migration; legacy DBs heal as users reconnect).
  New findActiveIntegrationConnectionByIdentity finder, scoped per
  (provider, owner), case-insensitive on either field.
- api-server: createConnection looks for an active match by identity
  before creating a fresh row. On hit it preserves connection_id so
  every integration_binding pointing at it stays valid; only volatile
  fields (external_id, secret_ref, label, scopes) refresh.
- desktop main: composioFinalize wrapper does whoami before posting and
  threads handle/email/displayName through. Identity propagation is
  transparent to the four renderer call sites.

Phase 2 — install-time account selector
- AppCatalogCard accepts availableAccounts / selectedConnectionId /
  onSelectAccount. Inline Select renders only when ≥ 2 accounts; one
  account = silent auto-bind; zero = existing "connect first" dialog.
- workspaceDesktop exports APP_TO_PROVIDER_MAP + getProviderForApp;
  installAppFromCatalog accepts an optional connectionId and writes
  integration_bindings (target_type=app, target_id=appId) atomically
  after the app install succeeds.
- AppsGallery groups active connections by provider, tracks per-card
  selection in local state, refreshes after install completes.
- AppSurfacePane integration picker reworked: provider-icon trigger
  with handle as primary line and email / label as secondary
  disambiguation, big enough to read multi-account flows at a glance.

Phase 3 — retroactive merge for users already affected
- Legacy NULL-identity rows were unreachable to the dedupe finder, so
  re-auth still spawned duplicates after Phase 1. Two new escape hatches:
  - composioFinalize backfill: before posting, list (provider, owner)
    rows with NULL identity, whoami each via Composio, PATCH the result
    back so dedupe can match on the next finalize.
  - IntegrationsPane auto-reconcile: after enrichment populates
    accountMetadata for *all* probeable rows, group by (provider,
    resolved-identity); for any group with ≥ 2 rows, keep the oldest,
    backfill identity, call merge to repoint bindings + drop the rest.
    Guarded with reconcileInFlightRef + cancellation flag for unmount.
- New runtime mergeConnections({ keepConnectionId, removeConnectionIds })
  validates (provider, owner) match, repoints every binding inside a
  SQLite transaction (atomic — half-merges can't survive a crash),
  rejects cross-provider/owner merges so unrelated accounts can't
  accidentally absorb each other's bindings.
- New routes: PATCH /connections/:id (now identity-aware) +
  POST /connections/:id/merge.

Misc
- workspaceApps.ts drops the static AppToolDefinition arrays — every
  app's MCP server already advertises tools dynamically; the snapshots
  drifted whenever an app shipped new ones. AppSurfacePane drops the
  "Tools (N)" section that consumed them.

Tests
- state-store: identity columns persist + null when missing,
  findActiveIntegrationConnectionByIdentity (provider/owner scoping,
  case-insensitive, skip inactive, both-keys-supplied), legacy DB
  migration path adds the columns + indexes idempotently. 70/70.
- api-server: dedupe-on-reconnect preserves connection_id and bindings,
  no-identity rows still create fresh, dedupe is provider-scoped,
  identity backfill via updateConnection, merge repoints across
  multiple workspaces, merge rejects cross-provider. 501/501.

Manual smoke (against my local DB with the bug active):
- 2 @jotyy GitHub rows on launch → IntegrationsPane reconcile fires
  after enrichment → merged into 1 row, both prior bindings preserved.
- New re-auth → no duplicate row, existing row's external_id refreshed.
- Marketplace install with 2 active accounts → inline picker, default
  most-recent, binding written for the chosen account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(desktop): boot-timing instrumentation for startup-speed work

Adds a BootTimer module that records milestone timestamps relative to
app.whenReady(), prints an aligned summary on boot completion, and
persists each launch's timings as JSON under userData/boot-timings/
so we can diff across optimisation passes.

Marks (main process):
  whenReady-start            (start of app.whenReady)
  db-bootstrap-done          (after bootstrapRuntimeDatabase)
  ipc-handlers-registered    (just before createMainWindow)
  main-window-created        (BrowserWindow constructed)
  main-window-ready-to-show  (first paint)
  browser-service-ready      (desktop browser subprocess up)
  runtime-spawn-start        (sidecar spawn dispatched)
  runtime-healthy            (first /healthz 200)
  first-list-workspaces      (first listWorkspaces resolves)

Marks (renderer):
  renderer-hydrated          (hasHydratedWorkspaceList flips true →
                              splash unmounts; triggers summary)

Wired through electronAPI.boot.{mark,getTimings} so future renderer-
side milestones (e.g. first-paint-after-data) can be recorded the
same way. No behaviour change; instrumentation only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): cut splash 1s — return cached runtime status, drop poll

Two coordinated changes to recover the 1.6s gap measured in baseline
boot timings between runtime-healthy and first-list-workspaces:

B1. runtime:getStatus IPC now returns the cached runtimeStatus
    directly instead of round-tripping through refreshRuntimeStatus()
    which always probes /healthz. During boot the sidecar isn't up
    yet, so each probe ate a 1500ms HTTP timeout. Push events
    (`runtime:state`) keep the cached value live — renderer gets a
    zero-latency stream and the IPC for the same state.

B2. Removed the 1s renderer-side polling loop in workspaceDesktop.tsx
    that re-queried runtime:getStatus while status === "starting".
    The push event already fires the moment the sidecar flips to
    "running"; the poll could only *delay* observed ready by up to
    a full tick (waiting for the next 1s boundary).

Baseline measurement (5.15s total to splash unmount):
  runtime-healthy            3485ms
  first-list-workspaces      5151ms  ← 1.66s gap to fix here

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(desktop): hydrate splash from local SQLite — 5.2s → 2.2s

The "Preparing your desktop" splash was gated on the runtime sidecar
spawning + first /api/v1/workspaces query — 3-4s of cold-start work
the user had to stare at on every launch. Three coordinated changes
collapse the wait to under the dev-mode Vite floor (~2.2s; prod will
land closer to ~1s):

1. Decouple workspace load from bootstrap. The workspace-load effect
   used to wait on `isLoadingBootstrap`, which fetches runtimeConfig
   + clientConfig (sidecar IPCs that on cold launch retry against a
   not-yet-healthy sidecar for 1.5s+). None of that bootstrap data is
   needed to render the workspace list. Effect now only gates on
   `runtimeReadyForWorkspaceData`; bootstrap data lands in state in
   the background.

2. Static splash in index.html. Window paints at ~1s but the React
   tree (and its WorkspaceBootstrapPane) used to take another ~450ms
   to mount, leaving a blank window. Inlined matching markup (logo +
   halo + dots animation) so the splash is visible the moment the
   window shows; App.tsx removes the static element via
   useLayoutEffect once React commits — no flash.

3. Optimistic local hydration. Desktop and runtime share runtime.db;
   the desktop main process opens it during bootstrapRuntimeDatabase
   to keep its own workspaces table mirror. New
   listWorkspacesFromLocalDb() reads that mirror directly (read-only
   SQLite handle, ~5-15ms total including IPC), bypassing the sidecar
   entirely. New optimistic-hydration effect on mount fires the cached
   IPC; if any rows come back, splash unmounts immediately. Sidecar
   still spawns + the regular workspace-load effect still runs, doing
   reconciliation in the background. Fresh-install / no-rows case
   falls through to the existing sidecar path, no behaviour change.

Measured (dev mode, cold launch, baseline boot-timing JSON archived):
  before: renderer-hydrated  5151-5984ms (high variance)
  after:  renderer-hydrated  2195ms
  delta:  ~3.6s saved (62%)

Remaining 2.2s is dominated by Vite dev-mode bundle load + React
mount; production builds skip both and should land closer to ~1s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): light-mode invisible text from splash CSS body color leak

Splash CSS in index.html set `color: #f5f5f5` on body (chosen via
prefers-color-scheme), which always wins over Tailwind's base
`text-foreground` for elements inheriting from body without an
explicit text-foreground class. When system was dark but app was on
the Holaos light theme, body color stayed near-white → SelectValue
inner span and any other inherited-coloured text went invisible on
white surfaces.

Scope the splash colours to `#boot-splash` itself; let Tailwind's
@apply text-foreground drive body color from the active app theme.

Bonus: SettingsMenuSelectRow forces `data-placeholder:text-foreground`
so the selected value isn't rendered in muted colour while Base UI's
items list is still unregistered (it lazy-mounts inside SelectContent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): drop divider under file identity header in preview

The horizontal line between the file name/meta row and the textarea
content was rendering visibly stronger than intended (and unwanted)
when previewing a file like workspace.yaml. Removed the border-b on
the identity header — content area's own padding already separates
the two sections cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "fix(desktop): drop divider under file identity header in preview"

This reverts commit 0f5715bd7d5fecb3cc632b06de6c00dc81e5a039.

* fix(desktop): tone down brand focus ring + drop boot-timing scaffolding

The global :focus-visible rule painted a 2px brand-coloured outline
plus a 3px box-shadow ring on every input/button/textarea. On full-
pane editors (file preview textarea) the outline rendered as a thick
orange line flush against the surrounding chrome — mistaken for a
divider, hard to ignore.

Switched to a single 2px box-shadow at 50% ring alpha (no outline,
so no offset bleed past overflow boundaries) and removed textarea
from the rule's targets — full-pane editors don't benefit from a
chrome-flush ring, and per-textarea components can opt back in if
they need one.

Also dropped the boot-timing instrumentation that was added during
the splash-speed work — the optimisation has landed, the JSON logs
under userData/boot-timings/ aren't useful day-to-day. Removed:
  - desktop/electron/bootTimer.ts
  - all bootTimer.{start,mark,complete} call sites in main.ts
  - boot:mark / boot:getTimings IPC handlers
  - electronAPI.boot.{mark,getTimings} preload + types
  - 6 renderer-side boot.mark calls in workspaceDesktop.tsx

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): integration account display rework — Phase 1 + Phase 2 Slice 1

Centralizes integration account metadata so handle/email/avatar render
consistently across IntegrationsPane, AppSurfacePane, and AppCatalogCard
without each surface re-fetching whoami on mount.

Phase 1 (frontend cache):
- integrationAccountStore.ts — module-level useSyncExternalStore cache;
  dedupes inflight fetches by account_external_id.
- integrationDisplay.ts — single canonical label precedence chain
  (meta.handle → conn.account_handle → meta.email → conn.account_email
   → meta.displayName → filtered conn.account_label → Account N).
- All three pickers migrated; per-pane local metadata state removed.
- Cache invalidated after dedupe-merge and disconnect.

Phase 2 / Slice 1 (per-provider whoami extraction):
- extractComposioIdentity normalizes Composio whoami across providers
  with explicit handlers for Twitter/X, GitHub, Reddit, LinkedIn, and
  Google variants; generic fallback probes common field names.
- composioFinalize and the legacy-row backfill loop both route through it.
- workspace:composioRefreshConnection IPC re-runs extraction against an
  existing connection's external id and persists newly-resolved fields.
- IntegrationsPane row gains a Refresh button next to Disconnect.

Plan: docs/plans/2026-04-28-integration-account-display-rework.md.
Phase 2 Slice 2 (schema columns + periodic refresh) and Phase 3
(server-side dedupe at finalize) are sequenced as separate work items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): dashboard file type + built-in renderer (workspace shared SQLite)

Design plan for treating dashboards as workspace files (.dashboard YAML)
rendered by a built-in desktop renderer over a workspace-shared SQLite
data plane. Marketplace stays scoped to integration apps; dashboards are
agent-authored artifacts, not separately-published apps.

Architecture:
- Single workspace/<id>/.holaboss/data.db (WAL, lockfile-protected).
- Apps write tables prefixed by app id (twitter_posts, linkedin_posts,
  reddit_posts) via a new @holaboss/bridge.getWorkspaceDb() helper.
- .dashboard panels: kpi (single-value card) + data_view (one query,
  multiple switchable views). v1 views: table + read-only board (Kanban).
- Two runtime-provided MCP tools for the agent: list_data_tables
  (introspects shared db) and create_dashboard (writes the YAML file).

Phased delivery:
1. Shared SQLite plumbing — Twitter pilot.
2. Renderer — kpi + data_view (table+board) views.
3. Agent authoring — list_data_tables + create_dashboard tools.
4. LinkedIn + Reddit migration.
5. Deferred: chart panels, gallery/calendar/timeline views, board drag,
   filters, visual editor.

All six pre-coding open questions confirmed:
- Renderer built into desktop.
- v1 panels = kpi + data_view(table+board).
- Board read-only in v1 (no drag-to-update).
- Files live at files/dashboards/*.dashboard.
- Migration: copy + rename + back up old per-app db.
- Agent schema awareness via list_data_tables tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(bridge): getWorkspaceDb helper for shared workspace SQLite

Adds a new SDK helper so module apps can write/read a single
workspace-shared SQLite file (workspace/<id>/.holaboss/data.db) instead
of each maintaining a private ./data/module.db. Same handle is reused
across calls within a process; the underlying file path is read from
WORKSPACE_DB_PATH (injected by the runtime — wired up in a follow-up).

- env.ts: resolveWorkspaceDbPath() returns the WORKSPACE_DB_PATH env value.
- workspace-db.ts: getWorkspaceDb() opens the file via better-sqlite3,
  enables WAL + foreign keys, creates parent dir, and caches the handle.
  Re-opens transparently if the env path changes between calls.
- better-sqlite3 declared as optional peerDependency so SDK consumers that
  only use the integration proxy don't pay the native-binding install.
- Tests cover env resolution and the missing-env guard. End-to-end exercise
  against a real db file lives in consuming app suites (the better-sqlite3
  native binding doesn't load under bun:test).
- Version bumped to 0.2.0-beta.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): inject WORKSPACE_DB_PATH for apps that opt in via env_contract

When buildShellLifecycleEnv builds an app process's environment, it now
adds WORKSPACE_DB_PATH=<workspace>/.holaboss/data.db whenever the app's
env_contract declares the variable. Apps then call the bridge SDK's
getWorkspaceDb() to read/write the workspace-shared SQLite, replacing
the per-app private ./data/module.db model.

- ts-runner-session-state.ts: workspaceDataDbPath(workspaceDir) helper,
  sibling to workspaceSessionStatePath. Both live under the existing
  `.holaboss` per-workspace folder.
- app-lifecycle-worker.ts: env_contract-gated injection of WORKSPACE_DB_PATH,
  matching the pattern already used for HOLABOSS_USER_ID and
  HOLABOSS_WORKSPACE_ID. Invalid workspace ids (sanitize throws) skip the
  injection silently — the underlying error surfaces elsewhere.
- One-line unit test pins workspaceDataDbPath's path layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bridge): restore syncAppResourceOutput export to match published 0.1.1-beta.1

The bridge source had drifted behind the published 0.1.1-beta.1 npm
artifact, which exports syncAppResourceOutput plus its
AppResourceOutputInput / AppResourceOutputResult types. Module apps
(twitter, linkedin, attio, …) all import these via their local
holaboss-bridge.ts shim, so building from source as-is would produce a
0.2.0 release that regresses every consumer.

Reconstructs the missing surface in source from the published d.ts and
js artifacts:
- AppResourceOutputInput / AppResourceOutputResult in types.ts.
- syncAppResourceOutput in workspace-outputs.ts — three-branch behavior
  (existingOutputId → PATCH, context → session artifact, neither →
  workspace output). No-op envelope when canPublishAppOutputs is false.
- Re-exported from index.ts.

Carries the version bump to 0.2.0-beta.0 introduced in the previous
commit; once both this and getWorkspaceDb land, 0.2.0 is publishable
with no consumer breakage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): install app from any local archive (dev tool)

Adds an "Install from file…" button on the Marketplace → Apps gallery
that lets a developer pick any .tar.gz / .tgz produced by
hola-boss-apps/scripts/build-archive.sh and install it directly,
without dropping the file into hola-boss-apps/dist/ first.

Flow:
1. Click button → Electron file picker opens with a .tar.gz / .tgz
   filter.
2. Main process reads app.runtime.yaml from the archive (system tar
   binary, no new Node tar dep) to resolve the app slug — no need for
   the user to type the app id.
3. If the picked file is outside the runtime's allowlist
   (os.tmpdir() / $HOLABOSS_APP_ARCHIVE_DIR / ~/.holaboss/downloads),
   it's copied into ~/.holaboss/downloads first, then cleaned up after
   install.
4. Same /api/v1/apps/install-archive request as the catalog path; the
   existing app-install-progress IPC events fire so the gallery state
   updates uniformly.

Cancelling the file picker resolves to null so the renderer can
distinguish cancel from error and avoid spurious refresh calls.

Errors (validation, manifest peek, install) surface inline above the
gallery instead of throwing — picks an app id from `slug` (preferred)
or `app_id` and rejects archives that declare neither.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): wire Remove app menu item via onClick (was a Radix-style onSelect)

The dropdown menu component is built on @base-ui/react/menu, where the
click handler is onClick — the same pattern every other DropdownMenuItem
in the codebase uses (AuthPanel, AutomationsPane, IntegrationsPane,
TopTabsBar, etc.). AppSurfacePane was the only place still using a
Radix-style onSelect callback, so the handler never fired and clicking
"Remove app" did nothing — the confirmation bar never appeared, the
DELETE never went out.

Drops the event.preventDefault() too — that was guarding against Radix
auto-closing the menu on select, but here we want the menu to close
once the destructive confirm bar opens, so closing is the right
behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bridge): switch getWorkspaceDb to static better-sqlite3 import

The previous createRequire(import.meta.url) pattern broke when consumers
re-bundled the SDK's ESM source into a CJS output (which is exactly
what the module-app build does — esbuild bundles start-services.cjs
with --format=cjs and pulls bridge's ESM entry, leaving import.meta.url
undefined at runtime). The MCP services process crashed at boot with
ERR_INVALID_ARG_VALUE before the workspace data.db was ever opened, so
the install-archive flow couldn't get the app to a healthy state.

Switching to a static `import Database from "better-sqlite3"` works in
both the SDK's own ESM/CJS dist and inside any downstream bundle —
better-sqlite3 stays a peer dependency, marked external by both
tsdown (via dependencies/peerDeps) and esbuild (via --external on the
consumer side), so the native binding still resolves through the
consumer's installed copy.

Drops the loadBetterSqliteSync wrapper now that the import is static.

Bumps bridge to 0.2.0-beta.1 — apps that pinned 0.2.0-beta.0 need to
update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): dashboard renderer for .dashboard YAML files (Phase 2)

Built-in renderer for the Notion-style structured dashboard format.
Picks up the workspace-shared SQLite from Phase 1 — every panel runs
its own SQL against workspace/<id>/.holaboss/data.db (read-only) and
renders into one of two panel kinds.

Architecture:
- main.ts runDashboardQuery — short-lived read-only better-sqlite3
  handle per call (PRAGMA query_only=ON). Caps result at 5k rows.
  Resolves the workspace data.db path through the existing
  resolveWorkspaceDir helper.
- IPC dashboard:runQuery returns { ok, columns, rows } | { ok, error }
  so a panel-level SQL failure surfaces inline without crashing the
  whole renderer.
- dashboardSchema.ts — hand-written YAML parser + validator (no zod
  dep). Supports kpi panels and data_view panels with table + board
  views. Each parse error names the offending panel/view index.

Components (all under components/dashboard/):
- KpiCard — single-value card. Convention: prefer column named `value`,
  fall back to first column of first row.
- TableView — column whitelist + order, paged at 500 visible rows.
- BoardView — read-only Kanban grouped by group_by; cards show
  card_title plus optional card_subtitle; per-column row cap of 200.
- DataViewPanel — owns active-view state, renders a tab bar when the
  panel has ≥ 2 views, dispatches to TableView or BoardView.
- DashboardRenderer — parses, runs all panel queries on mount/refresh,
  renders KpiCard or DataViewPanel per panel slot.

File integration:
- .dashboard added to TEXT_FILE_EXTENSIONS so readFilePreview returns
  YAML content as a text preview.
- FileExplorerPane: when a previewed text file's extension is
  .dashboard and a workspace is selected, render <DashboardRenderer>
  in place of the editor/markdown surface.
- File-icon mapping added so .dashboard shows a database glyph in the
  tree.

Smoke-test workflow:
- Hand-craft a `.dashboard` file under workspace/<id>/files/dashboards/.
- Click into it from the Files pane → KPI cards + DataView panel
  with table/board tabs render against twitter_posts.

What's deferred to Phase 3:
- Agent system tools list_data_tables and create_dashboard (so the
  agent can author dashboards from chat).
- Chart panels (line/bar/pie), gallery view, calendar/timeline,
  drag-to-update on the board, filter parameters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): wire dashboard renderer into InternalSurfacePane too

FileExplorerPane has a sibling preview surface, InternalSurfacePane,
that takes over for full-pane file views (the case actually exercised
when opening a .dashboard file from the workspace tree). I'd only
patched the FileExplorerPane branch, so the dashboard YAML kept
rendering through InternalSurfacePane's textarea fallback.

Adds the same `extension === ".dashboard"` short-circuit before the
textarea, importing the same DashboardRenderer. selectedWorkspaceId
is already in scope here via useWorkspaceSelection so no plumbing
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): retune dashboard renderer to Notion aesthetic

The cards-with-shadow + colored-header chrome from the first cut felt
heavy next to the rest of the workspace. Pass over each component to
shed surface weight: flat surfaces, subtle borders, generous whitespace,
text-led tab bar.

DashboardRenderer
- Drop the bg-muted/30 outer wrap and bg-background/60 sticky header.
  Page is plain bg-background with the title/description block sitting
  flush at the top of a centered 3xl column.
- Refresh demoted to a muted-foreground ghost button on the right.
- Consecutive kpi panels collapse into one CSS-grid row (up to 4 wide)
  so a Total/Published pair reads as a stat strip instead of two
  stacked cards.
- Larger inter-panel gap (gap-10) for breathing room.

KpiCard
- Strips border, shadow, and card background entirely. Just an
  uppercase muted label above a 3xl tabular-nums number.

DataViewPanel
- No surrounding card chrome. Title sits inline above a hairline
  border-b; tab bar lives in that same row, right-aligned.
- Tabs are now underlined text (border-b-2 on active) instead of pill
  toggles inside a bg-muted track.

TableView
- Removes zebra stripe (even:bg-muted/30). Headers gain an uppercase
  muted-foreground treatment; rows separated by a soft border-b/60
  hairline. Hover state is a single bg-muted highlight.
- Drops the per-cell px-3 padding in favor of py-2 pr-4 with a
  first:pl-0 reset so the table aligns flush with the title above it.

BoardView
- Columns are now plain stacks of cards — no surrounding bg-muted/40
  card with border. Just the title + count, then cards.
- Cards lose their border + shadow; bg-muted is enough to delineate.
  Hover swaps to bg-accent for affordance.
- Wider inter-column gap (gap-5) so the board breathes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): icon-button view switcher + roomier table rows

DataViewPanel
- Replaces the underlined-text Table/Board tabs with two square
  icon buttons (Table2 + Columns3 from lucide). Each button is
  size-7, rounded-md, with bg-muted active state and a hover swap —
  same shape language as the More-options button on the app surface
  toolbar so toolbars stay consistent across the desktop.
- title + aria-label keep the labels accessible without showing them
  on screen.

TableView
- Bumps row text from text-xs to text-sm and increases vertical
  padding (py-2.5) so the table reads less dense.
- Adds px-3 horizontal cell padding with first:pl-1 last:pr-1 resets
  so the table aligns flush with the panel's title above it.
- Soft hairline rows (border-b/40) with the last row left
  borderless — looks closer to Notion's table than the previous
  uniform hairline.
- Hover state lifted slightly to bg-muted/70 instead of bg-muted so
  the change is visible against an already-flat surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): list_data_tables + create_dashboard agent tools (Phase 3)

Two new system-level capability tools expose the dashboard authoring
loop to the agent: discover what's queryable in workspace data.db,
then write a `.dashboard` file that the renderer (Phase 2) picks up.

list_data_tables (GET /api/v1/capabilities/runtime-tools/data-tables)
- Opens the workspace's shared data.db read-only (PRAGMA query_only),
  walks sqlite_master + PRAGMA table_info for every non-internal
  table, and returns { tables: [{ name, columns, row_count }] }.
- Returns empty list with a note when data.db doesn't exist yet (no
  module app has written rows).

create_dashboard (POST /api/v1/capabilities/runtime-tools/dashboards)
- Validates the spec: title required, at least one panel, each panel
  is `kpi` (single-value) or `data_view` (with ≥1 view, table + board
  shapes), data_view's `default_view` enum-matches.
- Dry-runs every panel's SQL against data.db with `LIMIT 0` so a
  parse / column / table mistake surfaces immediately as a 400 with
  a typed error code, instead of silently writing a broken file.
- Sanitizes the slug ([a-z0-9._-]+, ≤80 chars), then writes
  workspace/<id>/files/dashboards/<name>.dashboard.

Tool registration plumbing
- harnesses/runtime-agent-tools.ts: id + description + policy entries.
- harnesses/runtime-capability-tools.ts: JSON-schema for tool params
  used by the agent's tool surface.
- harnesses/runtime-tool-capability-client.ts: maps tool ids to HTTP
  request plans.
- api-server/runtime-agent-tools.ts: wires the path/method into
  RUNTIME_AGENT_TOOL_DEFINITIONS, adds RuntimeAgentToolsService
  methods, plus helpers (sanitizeDashboardFileName,
  validatePanelInput, serializePanel).
- api-server/app.ts: GET + POST routes that delegate to the service.

Test coverage
- runtime-agent-tools.test.ts: 5 cases — empty data.db, populated
  introspection, end-to-end dashboard write + YAML readback,
  bad-SQL rejection (no file written), unsafe-slug rejection.

Also rolled in: a small linter pass on the dashboard TableView.tsx
that normalized text-[11px] → text-xs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): badges for status fields, segmented view switcher, card chrome on data_view, clamped long cells

Four refinements based on UX feedback while testing dashboards.

1. View switcher → segmented button group
   The Table/Board icons now live inside a single bordered track
   (border + bg-muted/40 + p-0.5) and the active button stands proud
   with bg-background + shadow-xs, mirroring the segmented-control
   shape used elsewhere in the desktop. Buttons themselves are
   slightly smaller (size-6 vs size-7) so the pair reads as a single
   compact control rather than two loose icons.

2. Status fields render as badges
   New StatusBadge component + name-based heuristic
   (`isStatusColumn`) — columns named status / state / stage / phase
   / lifecycle hand the cell value to a small pill colored by token:
   - published / active / completed / sent / done → bg-success/10
   - failed / error / cancelled / rejected → bg-destructive/10
   - queued / pending / processing / running → bg-info/10
   - scheduled / delayed / waiting / paused → bg-warning/10
   - draft / new / idle / open → bg-muted (neutral)
   - unknown values fall back to the muted neutral, never a wrong
     semantic color. BoardView's group-by header swaps the plain
     `<span>` for the same StatusBadge when the group_by column is
     status-like, so column names like "draft" / "published" /
     "failed" pick up matching tints.

3. data_view panel gets card chrome
   Each data_view now lives inside a rounded-lg bordered card with a
   header strip (title + row count + view switcher) above a
   border-b hairline, body sits below with px-4 pb-3 padding. KPI
   panels stay flat as before — their density is the inline-stat
   value, not card-level.

4. Long cells get a 2-line clamp + native tooltip
   Cells with content longer than 80 chars get line-clamp-2 with the
   full text as the `title` attribute, so a wall of tweet-length
   prose stops blowing up the row height. Status columns are
   exempted (always one line). Table is now table-fixed with a
   140px col for status columns so the badges line up neatly and
   text columns share the remaining width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): full-width toggle in dashboard header (Notion-style)

Default reading width bumps from max-w-3xl (768px) to max-w-4xl
(896px) so two-column tables and KPI rows breathe a bit more without
giving up the centered-document feel.

For wider data, a Notion-style toggle now sits left of the Refresh
button. Click flips the page between max-w-4xl and max-w-none
(genuine full width). State persists in localStorage under
`dashboardRenderer:fullWidth` so the chosen width sticks across
sessions per browser profile.

Icon swaps based on state — Maximize2 when compact, Minimize2 when
full width — with a corresponding label so the affordance reads
without needing the tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): lift dashboard toolbar to outer pane + icon-only with tooltips

The full-width and refresh buttons no longer share the centered
content's max-width — they now live in a sticky top-right strip
inside the pane's outer scroll container, anchored above the page
title regardless of the chosen page width. Switching to full width
no longer drags the controls across the screen.

Buttons are icon-only (size icon-sm) and use the existing Base UI
Tooltip component instead of native title attributes — tooltip
"Full width" / "Compact width" / "Refresh" appears on hover with
the bottom side aligned, matching how other icon-only toolbars in
the desktop are wired.

The strip uses a soft bg-background/85 + backdrop-blur so any
content scrolled under it stays legible without a hard divider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): move dashboard toolbar into the file preview header

The previous sticky strip sat inside the renderer body, which meant
two top-bars stacking on top of each other when a .dashboard file was
open: the file metadata header (filename, size, modified date) plus
the renderer's own controls. Lifting the controls into the file
header collapses both into one strip, the way Eye/Save already share
that surface for markdown / editable files.

InternalSurfacePane now owns:
- dashboardFullWidth (persisted to localStorage as before)
- dashboardRefreshKey (bumped on every Refresh click)
- Two icon-only Tooltip buttons rendered next to Eye/Save when the
  open file's extension is .dashboard.

DashboardRenderer no longer manages either piece of state — it
accepts `fullWidth` and `refreshKey` as props and renders the body
only. The host pane drives state changes via the lifted handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): post metrics convention — Twitter first, cross-platform shape

Lays out the schema convention every platform pack will follow:
<platform>_post_metrics (snapshot, PK post_id+captured_at),
<platform>_post_metrics_daily (rollup), <platform>_metrics_runs
(scheduler activity log), <platform>_api_usage (passive call counts).
Common columns impressions/likes/comments/shares mean the same thing
across platforms; platform-specific columns (bookmarks, unique_views)
allowed alongside.

Refresh model: app-internal scheduler (Option C from the design
discussion). The 5-min interval lives in twitter's start-services.ts
and calls a pure refreshPostMetrics() function — same function the
twitter_refresh_post_metrics MCP tool wraps for agent-driven manual
refresh. Workspace cron is left out of this loop because it routes
through the agent (LLM call per fire) which would burn ~$10/day in
cron-translation cost. Revisit when ≥ 3 apps need centralized
scheduling.

Tier policy lives in code (5min/30min/6h/1d/frozen by post age) so a
single cron tick can drive the whole schedule and tier transitions.

Backfill bound: only refresh posts < 7 days old at first launch;
older posts enter frozen tier. Agent can force-refresh via
{ post_ids, force: true }.

Pause via workspace.yaml `apps.twitter.metrics_refresh: enabled`,
plus a twitter_set_metrics_refresh MCP tool for chat-driven toggles.

Phased: M1 (this doc + schema) → M2 (refresh tool + tier logic) →
M3 (scheduler + backfill bound + pause flag) → M4 (daily rollup +
90d retention) → M5 (sample dashboard).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(dashboard): smooth full-width transition + better icons + hide app-internal tables from agent

Two issues from the metrics test pass.

1. Full-width toggle felt janky and the icons weren't right
- max-w-none animates over a literal-billion-px range under
  transition-[max-width], so the swap visibly didn't ease — replaced
  with a concrete max-w-[1600px] for the wide state.
- Added transition-[max-width] duration-200 ease-out on the centered
  content container so width changes glide instead of snap.
- Icons swapped from Maximize2 / Minimize2 (square diagonal arrows
  that read as "expand the whole window") to ChevronsLeftRight /
  ChevronsRightLeft (horizontal-axis arrows that read as "expand
  width" / "contract width"). Tooltip labels unchanged.
- Active state on the button now uses bg-muted text-foreground when
  full-width is on — the toggle itself reads as a press state, not
  just an icon swap, so the user sees the click took effect.

2. App-internal tables shouldn't show up in the agent's "what can I
   query?" view
- runtime list_data_tables now filters out tables whose name ends in
  one of the convention's app-internal suffixes (_jobs / _metrics_runs
  / _api_usage / _settings / _migrations). These are operational
  state for the apps themselves, not user-facing data — agents
  composing dashboards almost never need them and their visibility
  just adds noise to the schema list.
- Tool gains an optional include_system parameter (default false)
  for the rare case the agent really does want them. Response
  surfaces a hidden_system_count + a one-line note pointing at the
  override so the agent can re-query if it determines it needs them.
- Tool description rewritten to mention the filter; runtime client +
  capability schema updated to forward the new parameter.

Test coverage: new runtime-agent-tools.test.ts case seeds the four
internal table shapes alongside twitter_posts and twitter_post_metrics,
asserts the default response shows two tables with hidden_system_count
4, and asserts includeSystem=true shows all six.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): drop Tooltip wrapper on dashboard toolbar buttons

The Refresh + Full-width buttons were silently swallowing clicks under
the <TooltipTrigger render={<Button onClick=...>}> pattern in this
toolbar. Eye + Save in the same toolbar use plain <Button title="...">
and work, so collapse the dashboard pair to the same shape — drop
TooltipProvider / Tooltip / TooltipTrigger / TooltipContent in favor
of native title= tooltips.

Functionally identical user-facing affordance (hover shows the label),
none of the click-routing surprises that came from layering Base UI's
useRender on top of Button (which is itself a useRender wrapper around
ButtonPrimitive).

aria-pressed / aria-label preserved so accessibility doesn't regress.
The active-state visual feedback (bg-muted on full-width toggle)
stays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* debug(desktop): log dashboard toolbar state changes (TEMP — to be reverted)

* fix(desktop): share dashboard toolbar state across InternalSurfacePane instances

Two InternalSurfacePane instances mount simultaneously (chat surface
+ main display) when a .dashboard file is open. With useState, each
held its own dashboardFullWidth value; clicking the visible button
flipped one instance's state while the other kept rendering its stale
view, so the user saw the click "do nothing." Console logs confirmed
the divergence: clicked-instance's `before` value stayed pinned at
true while the other instance's effect log alternated.

Replaces the per-instance useState with a module-level store
(src/lib/dashboardToolbarStore.ts) read via useSyncExternalStore.
fullWidth and refreshKey now have one source of truth, every mounted
pane subscribes, and a single click updates every instance at once.
The localStorage persistence for the width preference moves into the
store too.

Click handlers on the toolbar buttons no longer wrap useCallback
locals — they call the store's exported toggleDashboardFullWidth /
bumpDashboardRefreshKey directly, so the JSX has no stale-closure
foothold.

Drops the temporary debug console.logs from the previous diagnostic
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): dedicated dashboard icon + label for .dashboard artifact cards

The agent-authored .dashboard files showed up in chat artifact cards
with the generic file-document treatment (gray tile + plain page
icon + "File · 3 KB · 5:08 AM" secondary text). Now they get a
first-class visual:

- New `dashboard` OutputVisualKind, branched off the same extension
  detection path that already handles spreadsheet / pdf / code /
  document so an output's `.dashboard` title is enough — no
  metadata category required.
- LayoutDashboard from lucide as the icon (the four-quadrant glyph
  reads as "dashboard" universally), tinted to the brand primary
  to match the app artifact treatment — they're peers in
  importance: app surfaces and dashboards are the two
  agent-authorable surface types in the workspace.
- outputKindLabel returns "Dashboard" for these files so the
  secondary line reads "Dashboard · 3 KB · 5:08 AM" instead of
  "File · …" — small but the cards now communicate what they are
  at a glance.
- Helper outputFileExtensionFromTitle factored out because
  outputKindLabel needs the extension before the metadata-aware
  outputFileExtension is called. Falls back to file_path metadata
  when title doesn't carry a suffix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): refresh integration connections on Reload + window focus

Same fix as a4dc9cf on feat/multi-account-integrations, ported to the
toolbar layout introduced by 68f89af.

The "Connect my account" CTA only fetched the connection list once on
mount, so users who reconnected an account in Settings and came back to
the app pane saw a stale empty list. The toolbar Refresh button only
swapped the iframe key — checkIntegration never ran again.

- handleRetry refetches integrations in parallel with refreshInstalledApps.
- onRetry's ready branch refetches integrations alongside the iframe reload.
- Adds a window-focus + visibilitychange listener (3s throttled) so the
  common OAuth path (Holaboss → browser → back) picks up the new
  connection automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): remount AppSurfacePane on appId change

AppSurfacePane carried local state (wasRemoved, frameUrl, frameLoading,
frameError, integrationContext) across appId changes because the two
render sites in AppShell didn't pass a key. Switching from one app to
another reused the same fiber, so stale state from the previous app
could leave the new app's pane visually empty until the user navigated
away (forcing an unmount) and back.

Adding key={appId} forces a fresh mount per app, matching the workflow
that already worked (switch to browser/files and back). Iframe reload
behavior is unchanged — the iframe was already keyed on frameUrl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): app surface toolbar uses AppIcon resolver instead of local-only providerIcon

The toolbar chip was calling providerIcon(appId, 22) directly, which
only knows about a hardcoded set of brand SVGs — anything outside that
list (e.g. Attio) silently fell through to a 2-letter initial chip even
though the sidebar's AppIcon would have found the logo on the Composio
CDN.

Adds a "toolbar" size variant to AppIcon (28px chip / 22px logo,
matching the existing toolbar styling) and points the AppSurfacePane
toolbar at it. The full backend → CDN → local-svg → letter fallback
chain now applies here too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(desktop): artifacts browser slides in from right with inbox-style cards

The "View all artifacts" overlay was a centered modal with single-line
button rows. Replaced with a right-edge slide-in pane (absolute inset-y-0
right-0, w-[360px], slide-in-from-right-4) that mirrors the Inbox shape:
ArrowLeft "back to chat" header, compact bordered card rows with an
uppercase kind eyebrow above the artifact title.

Behavior is unchanged from the user's POV — artifactBrowserOpen +
artifactBrowserFilter state still drives it, click still calls
onOpenOutput. Just the surface is now a side pane, not a centered modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): artifacts pane full-bleed; inbox + artifacts share slide-in animation

Two adjustments to the prior artifacts redesign:

1. Artifacts pane was a 360px right-anchored sliver; now fills the
   chat pane (absolute inset-0) so it visually mirrors how Inbox takes
   over the agent column.
2. Inbox was a hard React swap with no entry animation. Now both
   surfaces share animate-in fade-in-0 slide-in-from-right-3
   duration-200 ease-out, so opening either feels like the same
   side-pane gesture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): apply side-pane slide-in animation to all agentContent views

Extends the animate-in fade-in-0 slide-in-from-right-3 duration-200
ease-out treatment from inbox to the remaining agent-column views:
automations, app surface (AppSurfacePane), and internal surface
(InternalSurfacePane). AppSurfacePane and InternalSurfacePane don't
own their outer wrapper, so wrap them in a flex shell that owns the
animation.

Now any agentView.type switch into a non-chat surface lands with the
same gesture, matching what the user expects from the inbox flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): merge composer Pause pill into the send button slot

Two side-by-side buttons (Pause pill + ArrowUp send) felt redundant
during a response. Replaced with a single icon-sm slot that swaps
between Pause (Square / Loader2) when responding and Send (ArrowUp)
when idle. Both share the same rounded-lg footprint so the toggle
reads as one stable control.

Queue-while-responding via Enter in the textarea is unchanged — only
the visible button consolidated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): extend AppSurfacePane knownProviders fallback

The fallback map was stuck at the original 6 apps, so newly shipped
modules (calcom, attio, hubspot, apollo, instantly, zoominfo) had no
provider lookup until a binding already existed. Without it,
checkIntegration() set integrationContext=null and the surface's
"Connect <Provider>" chip didn't render at all — making it impossible
to bootstrap the first binding from the app surface itself.

attio appeared to work only because the user had already bound it via
the integrations pane; calcom had never been connected and so was
silently ungrabbable from its app surface.

* fix(desktop): cal.com toolkit slug is "cal" not "calcom"

Composio's catalog publishes the Cal.com toolkit under slug "cal" (the
card in the integrations pane is titled "Cal"). The provider-related
maps were previously keyed under "calcom" so they never matched the
real toolkit; harmless until we needed AppSurfacePane to render the
"Connect Cal.com" chip for the calcom app whose binding hadn't been
created yet.

Fixes:
- AppSurfacePane.knownProviders maps app id "calcom" → toolkit slug "cal"
- IntegrationsPane.PROVIDER_CATEGORY_GROUPS / TOOLKIT_PREFERENCE keyed
  on "cal", matching what providerIdForToolkit returns at runtime

* fix(desktop): drop fixed pixel widths on composer compact controls

The compact-mode wrappers were forcing the model picker and thinking
selector to specific pixel widths (compactModelControlWidth /
compactThinkingControlWidth). With short labels like "GPT-5.4" the
ModelCombobox trigger's w-full justify-between then pushed the chevron
to the far right of the wrapper, leaving a wide blank gap between the
label and the dropdown caret.

Both wrappers now just shrink-0 / fit content. The ThinkingValueSelect
still receives compactWidth so its icon-only branch keeps firing in
compact mode — only the outer width clamp goes away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(desktop): tighten chat composer textarea vertical padding

The textarea wrapper had 16/12 top/bottom padding (pt-4 pb-3) which left
visible dead space above the placeholder and a noticeable gap before the
footer. pt-3 pb-2 (12/8) reduces 8px of vertical air without crowding
the input cursor or hiding the focus state.

Horizontal asymmetry (px-5 textarea vs px-3 footer) is intentional —
keeps the placeholder caret column-aligned with the model picker brand
icon at the bottom — so left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime-client): extract typed SDK foundation

Adds @holaboss/runtime-client and ports the legacy inline runtime
helpers (requestRuntimeJsonViaHttp, requestRuntimeJson,
runtimeErrorFromBody, isTransientRuntimeError) out of
desktop/electron/main.ts into a standalone package. Migrates three
pure-forward call sites (listIntegrationCatalog, listCronjobs,
listNotifications) as a smoke test for the new pattern. Remaining
~55 call sites continue to work through a thin requestRuntimeJson
shim that routes through the singleton client.

Foundation only — Streams A/B/C will migrate the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime-client): extract typed SDK foundation

Adds @holaboss/runtime-client and ports the legacy inline runtime
helpers (requestRuntimeJsonViaHttp, requestRuntimeJson,
runtimeErrorFromBody, isTransientRuntimeError) out of
desktop/electron/main.ts into a standalone package. Migrates three
pure-forward call sites (listIntegrationCatalog, listCronjobs,
listNotifications) as a smoke test for the new pattern. Remaining
~55 call sites continue to work through a thin requestRuntimeJson
shim that routes through the singleton client.

Foundation only — Streams A/B/C will migrate the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(runtime-client): migrate Stream A pure-forward call sites to typed SDK

Migrate 23 requestRuntimeJson call sites in main.ts (lines 11000-12500)
to typed runtimeClient methods across five do…
- teach ts-runner to treat the user browser surface as opt-in for non-workspace sessions via request.context.use_user_browser_surface
- preserve existing workspace-session browser behavior while defaulting delegated sessions back to the agent browser
- add runner coverage for default delegated routing and explicit user-browser override
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli forwards the active browser surface as browser_space in the harness request|runTsRunnerCli defaults delegated sessions to the agent browser when the user browser is active|runTsRunnerCli honors explicit delegated requests to use the user browser surface" src/ts-runner.test.ts
- remove the remaining workspace-session special case that silently inherited the user browser whenever browser:user was active
- keep browser:user strictly opt-in through request.context.use_user_browser_surface
- add runner coverage for workspace-session default agent routing and explicit user-browser override
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli defaults workspace sessions to the agent browser even when the user browser is active|runTsRunnerCli defaults delegated sessions to the agent browser when the user browser is active|runTsRunnerCli honors explicit delegated requests to use the user browser surface|runTsRunnerCli honors explicit workspace-session requests to use the user browser surface" src/ts-runner.test.ts
…laboss-ai#230)

* feat: improve runtime evidence handling and browser extraction (holaboss-ai#208)

* refactor: keep turn results deterministic for evolve

- remove compacted_summary from turn_results storage, API payloads, and runtime callers so turn results stay an execution ledger only
- add semantic artifact reconstruction from persisted output events and session messages for evolve writeback and skill review
- stop using turn_results as semantic prompt input for post-run evolve/model stages and update the focused evolve doc
- update regression tests for schema removal and artifact-derived evolve context
- validation: npm run typecheck (runtime/state-store), node --import tsx --test src/store.test.ts, npm run typecheck (runtime/api-server), node --import tsx --test src/turn-memory-writeback.test.ts src/evolve.test.ts src/claimed-input-executor.test.ts src/session-checkpoint.test.ts src/app.test.ts src/ts-runner.test.ts

* feat: tighten prompt volatility and tool result replay shaping

- keep stable policy sections cacheable while routing volatile runtime context through context-message lanes

- add tool/browser preview shaping with clipping, spillover artifacts, and runtime/harness preview headers

- extend browser and terminal pagination fields plus untrusted-content boundary markers for page text

- wire runtime config surface for preview shaping controls and add/expand tests across api-server and harness layers

- validation: not run (not requested)

* feat: improve runtime evidence handling and browser extraction

- keep `turn_results` deterministic and execution-focused by removing semantic summary persistence and shifting evolve/model stages onto reconstructed artifacts instead of semantic turn-result fields
- add replay-budget telemetry, shared browser/runtime replay budgeting, and desktop diagnostics surfacing so token pressure and replay clipping are observable without relying on hidden prompt state
- strengthen scratchpad-first execution guidance so long-running investigations use session scratchpad as working memory while `todowrite` stays scoped to task coordination
- enrich `browser_get_state` with compact typed page facts and add `browser_extract_facts` as a deterministic browser helper for canonical metadata, claims, links, quotes, and numeric verification data
- wire browser capability definitions and host/runtime plumbing for the new browser helper and richer semantic browser outputs
- extend tests across state store, runtime API, browser tooling, and desktop diagnostics to cover deterministic turn results, scratchpad prompting, replay budgeting, and semantic browser extraction
- validation: `cd runtime/state-store && npm run typecheck && node --import tsx --test src/store.test.ts`
- validation: `cd runtime/api-server && npm run typecheck`
- validation: `cd runtime/api-server && node --import tsx --test src/turn-memory-writeback.test.ts src/evolve.test.ts src/claimed-input-executor.test.ts src/session-checkpoint.test.ts src/app.test.ts src/ts-runner.test.ts`
- validation: `cd runtime/api-server && node --import tsx --test src/agent-runtime-prompt.test.ts src/desktop-browser-tools.test.ts`
- validation: `./runtime/api-server/node_modules/.bin/tsx --test runtime/harness-host/src/pi-browser-tools.test.ts`
- validation: `cd runtime/api-server && node --import tsx --test src/harness-registry.test.ts --test-name-pattern "browser tools"`
- validation: `cd runtime/api-server && node --import tsx --test src/app.test.ts --test-name-pattern "browser capability preview mode spills screenshot data and trims browser_get_state lanes"`
- validation: `cd desktop && npm run typecheck && node --test --test-name-pattern "context-budget diagnostics" src/components/panes/ChatPane.test.mjs`

* fix: align browser capability description assertion with runtime wording

- update browser_get_state capability description expectation to match current manifest wording
- prevent runtime-api-server CI false negatives caused by stale legacy phrasing
- validate via npm --prefix runtime/api-server run test
- validate via npm --prefix runtime/api-server run typecheck
- validate via npm --prefix runtime/api-server run build

* feat: implement main-session hidden subagent orchestration

- add conversation bindings, hidden subagent runs, and main-session event queue persistence for the one-main-session model
- split front-of-house main-session prompt/tooling from executor subagents and route delegated/background work through hidden child sessions
- add runtime delegation, wait/cancel/resume APIs plus main-session event dispatch, legacy session export, cron remap, and background-task UI integration
- enforce PI harness tool projection so main sessions no longer leak disallowed tools such as web_search downstream
- validate with npm --prefix runtime/api-server run typecheck
- validate with npm --prefix desktop run typecheck
- validate with npm --prefix runtime/state-store run typecheck
- validate with node --import tsx --test src/pi.test.ts src/contracts.test.ts
- validate with node --import tsx --test ../harnesses/src/pi.test.ts

* fix: refresh main chat after stream close

- stop adopting unmatched stream done/error frames when a pending input exists
- refresh the active conversation when the matched stream closes so persisted assistant replies appear immediately
- add focused ChatPane coverage for the stream-close refresh path
- validate with npm --prefix desktop run typecheck
- validate with node --test --test-name-pattern='chat pane does not adopt unmatched done or error stream frames and refreshes after matching done' desktop/src/components/panes/ChatPane.test.mjs

* feat: harden main-session background task orchestration

- remove blocking `holaboss_wait_subagents` from the main-session runtime surface and replace it with `holaboss_get_subagent` and `holaboss_list_background_tasks`
- add capability routes and service support for background-task inspection while keeping cancel and resume on hidden subagents
- tighten main-session prompt and capability guidance so missing execution tools route to delegation instead of inline refusal
- fix main-session tool projection and legacy-history awareness so front sessions stay coordinator-focused and can inspect exported session history on demand
- harden queue/renderer behavior around stale claims and output streaming so replies do not get stranded until the next user input
- refine desktop background-task UX with inline cards, inspect-run entry, composer keyboard shortcuts, and simplified task panel copy
- update the Phase 1 implementation plan tracker and record the agreed inspection/model-routing rules
- validation: `npm --prefix runtime/api-server run typecheck`; `npm --prefix desktop run typecheck`; `node --import tsx --test src/app.test.ts --test-name-pattern='runtime subagent capability routes create and cancel hidden background tasks'` in `runtime/api-server`; `node --import tsx --test src/agent-runtime-prompt.test.ts` in `runtime/api-server`; `node --import tsx --test src/pi-runtime-tools.test.ts` in `runtime/harness-host`; `node --import tsx --test src/queue-worker.test.ts` in `runtime/api-server`; `node --test --test-name-pattern='chat pane does not adopt unmatched done or error stream frames and refreshes after matching done|chat pane opens a targeted postqueue stream for normal sends|chat composer supports ctrl-c draft cancel and arrow-up recall' desktop/src/components/panes/ChatPane.test.mjs`; `node --test desktop/src/components/panes/BackgroundTasksPane.test.mjs desktop/src/components/panes/ChatPane.background-tasks.test.mjs desktop/src/components/layout/AppShell.background-tasks.test.mjs`

* fix: harden main-session event delivery and subagent routing

- defer the main-session event worker's initial scan so embedded runtime startup can reach health before background processing begins
- make pending main-session event selectors require status='pending' so materialized rows are not reprocessed in a tight loop
- add regression coverage for materialized-event filtering and main-session event worker startup behavior
- block same-turn main-session polling of freshly delegated subagents while they are still queued or running
- tighten main-session prompt and capability guidance so delegation returns control instead of simulating waits via repeated get-subagent reads
- treat transient embedded-runtime startup failures more safely in the Electron notification path

- Validation:
- npm --prefix runtime/state-store run typecheck
- node --import tsx --test src/store.test.ts --test-name-pattern='main session event queue supports materialize deliver supersede lifecycle|main session pending selectors exclude materialized events' in runtime/state-store
- npm --prefix runtime/api-server run typecheck
- node --import tsx --test src/main-session-event-worker.test.ts in runtime/api-server
- node --import tsx --test src/app.test.ts --test-name-pattern='runtime subagent capability routes create and cancel hidden background tasks' in runtime/api-server
- node --import tsx --test src/agent-runtime-prompt.test.ts src/agent-capability-registry.test.ts in runtime/api-server
- npm --prefix desktop run typecheck
- node --test desktop/electron/main.notifications.test.mjs

* fix: stabilize runtime follow-up delivery and branch-local desktop state

- recover failed materialized main-session events and inherit the owner chat model/thinking for synthetic follow-up turns
- parse final runner stdout buffers as terminal events and surface valid terminal handler failures instead of misclassifying them as skipped output
- remove the hardcoded desktop dev userData override so branch-local env configuration can isolate runtime state cleanly
- Validation: npm run typecheck in runtime/api-server, runtime/state-store, and desktop; node --import tsx --test src/main-session-event-worker.test.ts src/runner-worker.test.ts in runtime/api-server; node -e "JSON.parse(require('fs').readFileSync('desktop/package.json','utf8')); console.log('package.json ok')"

* fix: harden main-session background delivery and front-of-house UX

- sanitize queued background-update containers before they reach the main-session prompt so front-of-house replies only see concise summaries plus deliverable metadata, not raw long-form artifact bodies
- tighten main-session reply doctrine to stay conversational, warmer, and more human while still delegating heavy work and avoiding long inline report or HTML dumps
- allow pending background completions to merge into the next normal main-session reply before the short timeout expires, while keeping autonomous follow-ups supplement-only
- materialize forwarded subagent deliverables back onto the owning main-session turn as artifacts instead of leaking them onto unrelated turns
- archive completed subagent sessions from the inline task list, add archive controls for terminal tasks, and simplify the background-task cards to title-plus-status presentation
- restore onboarding trace visibility while removing subagent-control tools from onboarding sessions
- scope artifact browsing to each assistant turn, dedupe duplicate forwarded outputs, and preserve chat send/placeholder state across refresh races
- keep historical session history readable after harness-ownership transfers by decoupling history reads from the live runtime binding row
- update the Phase 1 plan to reflect the short background merge window, supplement-only autonomous follow-ups, and the removal of user-facing progress updates
- validation:
  - npm --prefix runtime/api-server run typecheck
  - npm --prefix desktop run typecheck
  - node --import tsx --test src/agent-runtime-prompt.test.ts src/main-session-event-worker.test.ts src/claimed-input-executor.test.ts src/app.test.ts --test-name-pattern='composeAgentPrompt uses a conversational main-session prompt for workspace sessions|main-session event worker inherits the owner main session model and thinking for synthetic follow-ups|claimed input folds attached background updates into a normal user turn|queue route folds pending background updates into the next main-session input even before the merge window expires|runtime subagent capability routes create and cancel hidden background tasks|history endpoint returns stored messages even after runtime harness ownership transfers to another session'
  - node --test desktop/electron/background-tasks-ipc.test.mjs desktop/src/components/panes/BackgroundTasksPane.test.mjs desktop/src/components/panes/ChatPane.background-tasks.test.mjs --test-name-pattern='chat pane renders background tasks inline and removes the separate quick action|chat pane renders inline background tasks near the top of the pane|background tasks ipc|background tasks pane|main-session assistant turns suppress trace and thinking while onboarding and read-only inspection sessions keep internals'

* fix: tighten main-session delivery, cronjob scheduling, and desktop notifications

- add native macOS notifications for main-session reply completion while minimized or hidden, including reply snippets and click-to-focus behavior
- route system_notification cronjobs to native desktop notifications, remove cronjob rows from the in-app notification feed, and add dev-mode notification fallback plus lifecycle logging
- return Automations "Run now" actions back to the main chat so follow-up delivery lands in the front-of-house session
- harden main-session chat rendering with clearer separators, constrained assistant width, idle follow-up refresh, trace suppression for front-of-house sessions, and tighter per-message artifact ownership
- strengthen main-session background-update containers and prompts so follow-ups stay supplement-only, conversational, and artifact-driven instead of dumping long inline bodies
- fix cronjob scheduling to honor next_run_at for newly created jobs instead of executing immediately on first poll
- align cronjob child runs with delegated subagents by reusing normalized subagent tool-profile handling
- update the Phase 1 and Phase 2 plan to reflect the current hidden-subagent rollout and remaining UX polish work
- validation: npm --prefix desktop run typecheck
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix runtime/state-store run typecheck
- validation: node --test desktop/electron/main.notifications.test.mjs
- validation: node --test --test-name-pattern='app shell maps system-notification cronjobs to native desktop alerts instead of toast inbox items' desktop/src/components/layout/AppShell.test.mjs
- validation: node --test --test-name-pattern='scheduled rows expose a run-now action for each automation|new schedule button can route creation into the workspace chat' desktop/src/components/panes/AutomationsPane.test.mjs
- validation: node --test --test-name-pattern='assistant turns can use a soft structural band without adding bubble chrome' desktop/src/components/panes/ChatPane.test.mjs
- validation: node --import tsx --test src/claimed-input-executor.test.ts --test-name-pattern='claimed input creates a completion notification for successful cronjob session runs|claimed input creates a completion notification for failed cronjob session runs' in runtime/api-server
- validation: node --import tsx --test src/cron-worker.test.ts in runtime/api-server
- validation: node --import tsx --test src/main-session-event-worker.test.ts --test-name-pattern='main-session event worker inherits the owner main session model and thinking for synthetic follow-ups' in runtime/api-server

* fix: prevent empty main-session assistant bubbles

- filter assistant turns against session-specific visibility so execution-only turns hidden in the main session do not render as blank bands
- avoid committing live assistant messages when they contain only hidden execution internals and no visible output, artifacts, or memory proposals
- switch the chat render path to a visible-message list so separators and footer accessories ignore hidden-only assistant rows
- add ChatPane regression coverage for visibility-aware renderability and the live placeholder path
- validation: node --test --test-name-pattern='main-session assistant turns suppress trace and thinking while onboarding and read-only inspection sessions keep internals|live assistant turn keeps a plain status placeholder before any trace or output arrives|assistant turns can use a soft structural band without adding bubble chrome' desktop/src/components/panes/ChatPane.test.mjs
- validation: npm --prefix desktop run typecheck

* fix: unify visible notification routing and main-session prompt soul

- route main-session completion notifications through runtime notification records instead of direct chat-pane native alerts\n- enforce minimized-only native desktop notifications and use in-app toasts for visible cross-workspace runtime events\n- keep same-workspace visible main-session/system-notification events inline-only instead of double-notifying\n- add a dedicated Assistant soul section and tighten concise front-of-house prompt wording for the main session\n- add runtime notification coverage for main-session completions and update desktop notification policy tests\n- Validation: node --import tsx --test src/claimed-input-executor.test.ts --test-name-pattern='claimed input creates a completion notification for successful cronjob session runs|claimed input creates a completion notification for failed cronjob session runs|claimed input creates a completion notification for completed main-session runs'\n- Validation: node --test --test-name-pattern='app shell polls runtime notifications and renders the toast stack|app shell opens cronjob session-run notifications in the sub-session chat|app shell routes runtime notifications by window state and workspace visibility|chat pane no longer sends native desktop notifications directly for main-session completions|desktop bridge exposes native notifications for minimized main-session completions' desktop/src/components/layout/AppShell.test.mjs desktop/src/components/panes/ChatPane.test.mjs desktop/electron/main.notifications.test.mjs\n- Validation: npm run typecheck in runtime/api-server and desktop

* fix: clear chat live state on workspace switches

- fully reset chat-pane live-run state when the selected workspace changes to avoid carrying pending input and stream state across workspaces
- clear queued inputs, active session view, and hydrated desktop main session before attaching the new workspace
- add a regression check covering fast workspace switches so new-workspace sends do not queue behind the previous workspace run
- validation: npm --prefix desktop run typecheck
- validation: node --test --test-name-pattern='chat pane clears prior workspace live-run state immediately on workspace switch' desktop/src/components/panes/ChatPane.test.mjs

* fix: forward self-contained child outputs through background containers

- forward plain-text child terminal output through the background update container so the main session can reason from the child result without reopening the child trace
- keep stripping html-like bodies from queued background prompt payloads so raw artifact/report bodies still stay out of the main-session prompt
- strengthen hidden subagent session policy so child outputs are self-contained handoff artifacts and include actual items instead of only a lead summary
- add prompt regressions for background-output sanitization and the new self-contained subagent output doctrine
- validation: npm run typecheck (runtime/api-server)
- validation: node --import tsx --test src/agent-runtime-prompt.test.ts src/main-session-event-prompt.test.ts src/app.test.ts --test-name-pattern='composeAgentPrompt requires subagent outputs to stay self-contained|composeAgentPrompt uses a conversational main-session prompt for workspace sessions|queued main-session event prompt entry strips html-like child outputs|queued main-session event prompt entry forwards plain-text child outputs|queue route folds pending background updates into the next main-session input even before the merge window expires'

* feat: continue delegated work in existing subagent sessions

- add holaboss_continue_subagent runtime tool and API route so follow-up work can reuse the same child session
- route continuation requests through the runtime capability client and expose the tool only to main-session orchestration
- update main-session soul and routing prompts to ask on ambiguity and continue relevant child sessions instead of spawning fresh ones
- keep delegated child outputs self-contained while preserving child-session history for follow-up context
- suppress duplicate in-flight assistant history rows when attaching active subagent streams in the chat pane
- forward browser capability input ids through harness browser tooling for per-turn capability routing
- validation: node --import tsx --test src/runtime-agent-tools.test.ts src/agent-runtime-prompt.test.ts src/agent-capability-registry.test.ts src/ts-runner.test.ts
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix desktop run typecheck
- validation: node --import tsx --test src/pi-runtime-tools.test.ts --test-name-pattern='Pi runtime subagent tools normalize delegated task bodies and control routes|Pi desktop browser tools execute through the runtime capability API'

* feat: give main session assistant a Hola identity

- name the main-session assistant Hola in the runtime soul prompt
- strengthen the main-session character contract so Hola behaves like a real front-of-house teammate with taste, reactions, and opinions
- preserve concise chat behavior, ambiguity clarification, and artifact-first delivery guardrails
- label bound main-session assistant turns as Hola while preserving session titles for inspection views
- add prompt and chat-pane assertions for the Hola identity
- validation: node --import tsx --test src/agent-runtime-prompt.test.ts
- validation: node --test --test-name-pattern='main-session assistant turns are labeled as Hola|main-session assistant turns suppress trace and thinking while onboarding and read-only inspection sessions keep internals' desktop/src/components/panes/ChatPane.test.mjs
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix desktop run typecheck

* fix: preserve delegated blocker state and broaden fallback routing

- teach main and subagent prompts to try viable delegated/browser/web/file routes before reporting missing integrations
- let subagent runs ask recoverable user-blocker questions and infer waiting-on-user state from login/auth/access blockers
- preserve persisted waiting-on-user blocker state during background-task sync so runs do not appear completed while awaiting user action
- update delegated-task tool guidance and run-specific fallback context for concrete lookup/stat/research requests
- add regression coverage for fallback prompt context, blocker inference, and background-task status reconciliation
- validation: node --import tsx --test src/runtime-agent-tools.test.ts --test-name-pattern='preserves persisted waiting-on-user blockers|continueSubagent|cancelSubagent waits'
- validation: node --import tsx --test src/claimed-input-executor.test.ts --test-name-pattern='recoverable login blockers|waiting-on-user subagent blockers'
- validation: npm --prefix runtime/api-server run typecheck

* feat: expand embedded browser efficiency and observability

- add revision-aware browser state with compact unchanged responses and browser storage/cookie helpers
- add lightweight browser observability tools for console output, recent errors, request summaries, and request drill-down
- wire Electron browser-session bridges, runtime telemetry, embedded browser skills, and runtime docs updates
- add and update browser runtime, claimed-input, turn summary, and harness host coverage for the expanded tool surface
- Validation:
  - runtime/api-server: node --import tsx --test src/desktop-browser-tools.test.ts src/workspace-skills.test.ts
  - runtime/api-server: node --import tsx --test src/claimed-input-executor.test.ts
  - runtime/harness-host: node --import tsx --test src/pi-browser-tools.test.ts
  - runtime/api-server: npm run typecheck
  - desktop: npm run typecheck

* fix: harden main-session coordinator delegation semantics

- clarify that the main session has only a partial direct capability surface and should treat surfaced tools as its full direct authority for the run
- state that operator surfaces are continuity context rather than implicit authority to mutate or control them
- surface front-session delegation guidance in the capability availability snapshot when delegated execution is available
- update runtime prompt and config tests to match the stronger coordinator/delegation semantics
- validate with node --import tsx --test src/agent-runtime-prompt.test.ts src/agent-capability-registry.test.ts src/ts-runner.test.ts
- validate with node --import tsx --test src/agent-runtime-config.test.ts (1 pre-existing unrelated todo-continuity assertion still failing)

* fix: restore chat pane completion feedback and test alignment

- add a local main-session completion chime in ChatPane using Web Audio
- suppress the chime for onboarding, read-only inspection, and paused runs
- dedupe completion chimes per workspace/session turn so refreshes do not replay them
- reconcile ChatPane source-shape assertions with the merged browser-upgrade implementation
- keep the full ChatPane source-regex suite green after the layout and browser-session UI changes
- validation: node --test desktop/src/components/panes/ChatPane.test.mjs

* fix: stabilize runtime prep and inline background task rail

- add the delegated capability prompt section id to the shared harness prompt type union so desktop runtime preparation builds successfully again
- move the in-progress background task pill into an overlay inside the chat body so it no longer pushes the message pane downward
- update chat pane source-shape tests to match the overlay layout and inline background task rendering
- validate with targeted chat pane tests and npm run desktop:prepare-runtime:local

* fix: restrict delegated use of the user browser surface

- merge the main-branch browser-space gate so non-front sessions default back to the agent browser unless request.context.use_user_browser_surface is set
- detect explicit delegated requests for the user current/shared browser surface and stamp child input context with use_user_browser_surface
- preserve that flag across subagent continue and resume flows so login-gated browser work stays on the same surface
- add service coverage for explicit opt-in and follow-up preservation, plus runner coverage for delegated default vs explicit override
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli forwards the active browser surface as browser_space in the harness request|runTsRunnerCli defaults delegated sessions to the agent browser when the user browser is active|runTsRunnerCli honors explicit delegated requests to use the user browser surface" src/ts-runner.test.ts
- validation: ../api-server/node_modules/.bin/tsx --test src/pi.test.ts
- validation: node --import tsx --test --test-name-pattern "delegateTask opts into the user browser surface only for explicit current-browser requests|continueSubagent preserves the user browser surface flag for follow-up work|resumeSubagent preserves the user browser surface flag while waiting on user access|continueSubagent queues a new input onto the same completed child session" src/runtime-agent-tools.test.ts

* fix: make delegated user-browser elevation explicit

- replace delegated browser-surface text heuristics with an explicit use_user_browser_surface flag on holaboss_delegate_task inputs
- extend runtime tool schemas, runtime tool HTTP normalization, and harness client serialization to carry the explicit browser-surface flag end to end
- preserve the explicit user-browser flag across subagent continue and resume flows without inferring it from free-form follow-up text
- teach the main-session prompt and delegate-task tool guidance to default browser work to the agent browser and set the flag only when the user explicitly asks for their current or shared browser context
- add targeted tests for prompt guidance, delegate-task transport, explicit opt-in, and follow-up preservation
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli defaults workspace sessions to the agent browser even when the user browser is active|runTsRunnerCli defaults delegated sessions to the agent browser when the user browser is active|runTsRunnerCli honors explicit delegated requests to use the user browser surface|runTsRunnerCli honors explicit workspace-session requests to use the user browser surface" src/ts-runner.test.ts
- validation: node --import tsx --test --test-name-pattern "composeAgentPrompt uses a conversational main-session prompt for workspace sessions" src/agent-runtime-prompt.test.ts
- validation: ../api-server/node_modules/.bin/tsx --test --test-name-pattern "runtime delegate-task client forwards use_user_browser_surface when explicitly requested" src/capability-http.test.ts
- validation: node --import tsx --test --test-name-pattern "delegateTask opts into the user browser surface only when explicitly requested|continueSubagent preserves the user browser surface flag for follow-up work|resumeSubagent preserves the user browser surface flag while waiting on user access|continueSubagent queues a new input onto the same completed child session" src/runtime-agent-tools.test.ts

* fix: align runtime api tests with current state-store types

- cast untyped context_budget_decisions event payloads through a record helper in claimed-input-executor tests so TypeScript matches the runtime event shape
- remove the stale compactedSummary field from turn-result-summary test fixtures because TurnResultRecord no longer exposes it
- keep the runtime behavior unchanged and limit the change to test typing drift that was breaking runtime-api-server CI typecheck
- validation: npm --prefix runtime/api-server run typecheck

* fix: align ts-runner skill staging test with bundled browser skills

- update the workspace-skill staging assertion in ts-runner tests to include bundled browser-use skills now present in CI\n- keep the test aligned with the current embedded skill ordering for pi harness requests\n- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli resolves workspace skill ids and source directories for the pi harness" src/ts-runner.test.ts

* fix: correct delegated browser capability snapshot for main sessions

- stage delegated browser tools separately with subagent session semantics instead of reusing the front-session browser tool list\n- pass delegated browser and extra-tool surfaces through the runtime-config request so main-session prompts describe real subagent browser capability\n- tighten ts-runner regression coverage to match live front-session browser staging behavior\n- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli strips staged execution tools from front-of-house workspace sessions|runTsRunnerCli defaults workspace sessions to the agent browser even when the user browser is active|runTsRunnerCli stages browser tools for subagent executor sessions and strips orchestration runtime tools" src/ts-runner.test.ts\n- validation: node --import tsx --test --test-name-pattern "projectAgentRuntimeConfig includes delegated executor capability context for main workspace sessions" src/agent-runtime-config.test.ts

* fix: update ts-runner bootstrap stage expectation

- include the delegated browser staging step in the structured-output ts-runner bootstrap timing assertion\n- keep the runtime-api-server test aligned with the delegated browser capability snapshot bootstrap path\n- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli only advertises structured output when the selected harness supports it" src/ts-runner.test.ts

* fix: centralize delegated kickoff phrasing in main-session delivery policy

- keep fresh delegation state-language ownership in the main-session response delivery policy only
- remove duplicated kickoff phrasing pressure from execution, routing, and tool-guidance layers so Hola does not stack near-identical acknowledgements
- teach the main session to separate accepted, in-progress, waiting, and completed states and to reserve completion language for real terminal results
- add prompt assertions covering single-line kickoff, waiting-on-user phrasing, and completion merge behavior
- validation: node --import tsx --test src/agent-runtime-prompt.test.ts
- validation: node --import tsx --test src/agent-capability-registry.test.ts
…ames (holaboss-ai#231)

Window-state events ('maximize', 'unmaximize', 'minimize', 'restore',
'enter-full-screen', 'leave-full-screen', 'ready-to-show') can fire
after the renderer has begun teardown, producing a noisy stderr line:

  Error: Render frame was disposed before WebFrameMain could be accessed
    at emitWindowStateChanged (.../main.cjs:5583:30)
    at BrowserWindow.<anonymous> (.../main.cjs:18009:5)

Two distinct disposal states race with window teardown:
  1. WebContents fully destroyed — webContents.isDestroyed() === true.
  2. WebContents alive but the underlying RenderFrame (WebFrameMain)
     has been disposed mid-teardown — isDestroyed() still returns false
     and send() throws the "Render frame was disposed" message.

resolveTargetWindow() already guards the BrowserWindow itself but not
the WebContents. Add an isDestroyed() short-circuit for case 1 and
wrap send() in try/catch for case 2 (RenderFrame state isn't
introspectable from outside). Rethrow anything else so we don't
accidentally swallow real bugs.

No behavior change beyond suppressing the throw on disposed frames.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- add inline PowerPoint preview extraction and rendering across desktop file and internal surface panes
- extend Electron preview payload types and tests to cover presentation artifacts alongside existing table previews
- remove direct MCP server and tool exposure from front workspace sessions while preserving delegated subagent access
- tighten main-session delegated response guidance to avoid duplicate kickoff/completion phrasing
- validation: npm --prefix desktop run typecheck
- validation: npm --prefix runtime/api-server run test (fails in existing suites because better-sqlite3 could not be resolved in app/automation/queue/runtime-agent-tools test modules)
…boss-ai#232)

* feat(runtime): workspace data layer (Tier 1 + Tier 2)

Tier 1: runtime owns workspace `data.db` lifecycle. The runtime now
opens, WAL-configures, and tracks the per-workspace SQLite shared by
agents and apps, instead of relying on whichever app happens to spawn
first. Adds `ensureWorkspaceDataDb` helper used by ts-runner session
state and runtime agent tools.

Tier 2: declarative app schemas. Apps may declare a `data_schema:`
block in `app.runtime.yaml`; the runtime applies it before the app
spawns, idempotently, with `_app_schema_versions` tracking and an
adopt-existing-tables fallback for apps that wrote their tables prior
to declaring a manifest.

- runtime/api-server/src/data-schema.ts                 (new)
- runtime/api-server/src/apply-app-schema.ts            (new)
- runtime/api-server/src/app-lifecycle-worker.ts        (calls maybeApplyAppSchema before spawn)
- runtime/api-server/src/ts-runner-session-state.ts     (ensureWorkspaceDataDb)
- runtime/api-server/src/runtime-agent-tools.ts         (list_data_tables uses ensure)
- runtime/api-server/src/workspace-apps.ts              (read schema from manifest)
- docs/plans/2026-04-30-workspace-data-layer-tier2.md   (design)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dashboard): v2 — Notion-quality panels (kpi/chart/table/board/list/text)

Re-grounds the desktop dashboard renderer on a v2 grammar designed
against Lark/Notion/Airtable. Panels: kpi, stat_grid, chart, data_view,
text. Views (under data_view): table, board, list — calendar/timeline/
gallery deferred. Width-flow grouping (full/half/third), parallel-query
KPIs, and a single shared format module so every panel + view formats
values the same way.

Highlights:
- ChartPanel — line / bar / area / pie / donut on Recharts, sky+orange
  series palette using Tailwind v4 OKLch values.
- TableView — column formats / colors / link templates / sortable headers
  / drag-to-resize columns / scroll-aware horizontal edge fade. Default
  widths tuned for chip / numeric / format columns vs title/name.
- BoardView — group_colors map, hash-color fallback, runtime group_by
  switcher (Notion-style picker, Base UI DropdownMenu using onClick).
- ListView + KpiCard + StatGridPanel + TextPanel — unified visual
  language, font-mono numerics, layered shadow scale (shadow-sm…2xl).
- format.ts — Notion-style formatSmartDate (relative-for-recent +
  abbreviated-absolute), looksLikeDateColumn auto-detection, color
  token classes, hashToColor palette.
- index.css — bg-fg-N color-mix utilities, custom scrollbar, 9-cube
  spinner, shimmer, smooth corners (corner-shape: superellipse),
  scroll-aware mask vars, theme-aware shadow-alpha.
- Added @fontsource-variable/geist-mono on the desktop side for
  numerics; recharts as a desktop dep.

- desktop/src/components/dashboard/*                    (BoardView, ChartPanel, DashboardRenderer, DataViewPanel, KpiCard, ListView, StatGridPanel, StatusBadge, TableView, TextPanel, format.ts)
- desktop/src/lib/dashboardSchema.ts                    (v2 grammar parser)
- desktop/src/index.css                                 (utilities, animations, theme)
- desktop/package.json + lock                           (recharts, @fontsource-variable/geist-mono)
- docs/plans/2026-04-30-dashboard-v2-design.md          (frozen design)
- docs/plans/2026-04-30-dashboard-panels-v2.md          (Lark/Notion/Airtable comparison)
- docs/plans/2026-04-30-dashboard-craft-quality-gap.md  (vs craft-agents-oss)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(desktop): marketplace BFF SDK URL + protocol type sync

Two independent fixes that accumulated on the branch:

1. Marketplace SDK base URL points at the Hono BFF (`/api/marketplace`)
   rather than the Python control-plane proxy (`/gateway/marketplace`).
   New `marketplaceBffBaseUrl()` helper, wired through
   `getMarketplaceAppSdkClient()` and the `auth:getMarketplaceBaseUrl`
   IPC. Resolves the renderer-facing 404 on marketplace SDK calls.

2. `BrowserStatePayload` / `BrowserTabLifecycleState` /
   `BrowserControlMode` aligned with `main.ts`'s ambient declarations
   (narrower lifecycle states, nullable `sessionId`, `error: string`).
   Leftover from the earlier merge reconciliation.

(The branch also carried a window-state-emit RenderFrame guard, but
that fix was already merged on main as holaboss-ai#231; the rebase auto-folded
it into a no-op.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olaboss-ai#233)

* refactor(desktop): extract browser observability helpers

Move the pure observability layer — types + formatting / parsing helpers
that the per-tab listeners and HTTP routes both depend on — into a new
module browser-pane/observability.ts. Stateful per-tab append/upsert
helpers stay in main.ts (they need the tab record + workspace map).

Moved:
- types: BrowserConsoleLevel, BrowserErrorSource, BrowserConsoleEntry,
  BrowserObservedError, BrowserRequestBodyMetadata,
  BrowserResponseBodyMetadata, BrowserRequestRecord
- constants: BROWSER_OBSERVABILITY_ENTRY_LIMIT, BROWSER_REQUEST_HISTORY_LIMIT,
  BROWSER_OBSERVABILITY_DEFAULT_LIMIT
- pure fns: browserObservabilityLimit, browserConsoleLevelValue,
  browserConsoleLevelRank, browserObservedErrorSource,
  browserIsoFromNetworkTimestamp, browserHeaderRecord,
  browserHeaderFirstValue, browserResponseBodyMetadata,
  browserRequestBodyMetadata, appendBoundedEntry, browserRequestIdValue,
  browserRequestFailure, browserRequestSummary

main.ts -195. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): extract per-tab observability state mutators

Move the stateful side of browser observability — the listener
callbacks that record into a tab's consoleEntries / errorEntries /
requests / requestOrder — into a new module
browser-pane/tab-observability.ts. The pure formatting layer
(observability.ts) and the tab record's state shape stay where they
were; only the upsert / track / lookup logic moved.

Moved:
- browserTabForWebContentsId
- appendBrowserObservedError
- upsertBrowserRequestRecord
- trackBrowserRequestStart / Headers / Completion / Failure

main.ts adds an `eachBrowserTabRecord()` generator that walks every
workspace + space + agent-session-space, passed as the only dep to
createTabObservability(). Module surface stays small (1 dep).

main.ts -240. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): extract user-lock + interrupt prompts

Move per-workspace BrowserUserLockState management, agent-interrupt
confirmation dialog, and the per-WebContents programmatic-input depth
counter into browser-pane/user-lock.ts.

Moved:
- activeUserBrowserLock / releaseUserBrowserLock / ensureUserBrowserLock
- pauseBrowserControlSession
- agentBrowserSessionNeedsInterrupt / confirmBrowserInterrupt
- maybePromptBrowserInterrupt
- isProgrammaticBrowserInput / withProgrammaticBrowserInput

The dedup Set (`userBrowserInterruptPrompts`) and the WeakMap counter
(`programmaticBrowserInputDepth`) are closure-captured by the factory.
Runtime-status awareness is injected as an `isAgentSessionBusy`
callback so the module stays decoupled from the runtime client.

main.ts -207. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): extract agent-session lifecycle

Move per-agent-session browser tab-space lifecycle into a new module
browser-pane/agent-session-lifecycle.ts:

- hydrateAgentSessionBrowserSpace
- suspendAgentSessionBrowserSpace
- scheduleAgentSessionBrowserLifecycleCheck
- touchAgentSessionBrowserSpace
- reconcileAgentSessionBrowserSpace

The reconcile loop's runtime-status query is injected as a
fetchSessionRuntimeStatus callback, hiding listRuntimeStates +
runtimeRecordEffectiveStatus behind a small adapter on the main side.

main.ts -125. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): extract browser tab state — read+emit + lifecycle

Move the tab subsystem (~30 functions) into a single new module
browser-pane/tab-state.ts:

read+emit:
  hasVisibleBrowserBounds, applyBoundsToTab, updateAttachedBrowserView,
  emitBrowserState, emitHistoryState, syncBrowserState, captureVisibleSnapshot,
  setBrowserBounds, recordHistoryVisit, browserPagePayload,
  closeBrowserTabRecord, getActiveBrowserTab, activeVisibleBrowserTarget,
  currentBrowserTabPageTitle, currentBrowserTabUrl

lifecycle:
  focusBrowserTabInSpace, setActiveBrowserTab, closeBrowserTab,
  navigateActiveBrowserTab, handleBrowserWindowOpenAsTab,
  showBrowserViewContextMenu, createBrowserTab, initialBrowserTabSeed,
  ensureBrowserTabSpaceInitialized

download-prompt:
  browserContextSuggestedFilename, queueBrowserDownloadPrompt,
  consumeBrowserDownloadOverride

createBrowserTab carries the new browser observability listeners
(console-message → consoleEntries + observed errors, render-process-gone,
did-fail-load → observed errors via deps.appendBrowserObservedError).

Pattern: factory `createBrowserPaneTabState(deps)` returns a closure-bound
object. Module-level state in main.ts (mainWindow, attachedBrowserTabView,
browserBounds, active workspace/space/session) is reached through
getter/setter deps so this module never imports any mutable globals.

main.ts wraps three of the public methods (navigateActiveBrowserTab,
setActiveBrowserTab, closeBrowserTab) with thin async functions that
ensureBrowserWorkspace + touchAgentSessionBrowserSpace before delegating —
that orchestration stays main-side because it materializes workspaces.

main.ts -1126. Typecheck + 17 BFF unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): extract browser HTTP service with all 16 routes

Move the localhost HTTP server's route handler (handleDesktopBrowserServiceRequest
+ 7 helpers) into browser-pane/http-service.ts. The module owns:

- request header parsers (token / workspace / session / space)
- JSON read/write helpers
- serializeEvalResult
- handleRequest with all 16 routes:
  * GET /health, /tabs, /downloads, /console, /errors, /requests,
    /requests/<id>, /cookies, /page, /operator-surface-context
  * POST /navigate, /tabs, /tabs/select, /tabs/close, /cookies,
    /evaluate, /context-click, /mouse, /keyboard, /screenshot

Server lifecycle (start/stop, auth-token rotation, capability config
sync) stays in main.ts because it's tied to runtimeStatus and
emitRuntimeState. main.ts now wires a BrowserHttpService instance and
the createServer callback delegates to it.

main.ts -976. Typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(desktop): consolidate browser:* IPC handlers

Move the 25-channel ipcMain.handle("browser:*", ...) block into a new
module browser-pane/handlers.ts. Each handler is a thin delegator over
the already-extracted subsystems (tab-state, popups, user-lock,
bookmarks, downloads); main.ts now just calls
installBrowserPaneIpcHandlers(deps) at app startup.

Bonus: extract the user-vs-agent interrupt prompt + early-return path
into a shared interruptOrSnapshot() helper inside handlers.ts so the
same six "navigation while agent owns space" handlers no longer copy
the snapshot return manually.

main.ts -631. Typecheck + 17 BFF unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
- add a create_data_table runtime capability that writes user-facing tables into the shared workspace DB at .holaboss/data.db
- expose the new capability through the runtime API, harness tool schema, and runtime capability client for delegated agent use
- clarify dashboard and data-table tool guidance so agents know the runtime already provisions the shared DB and should create sample data before create_dashboard
- add regression coverage for shared data table creation and the empty-workspace demo dashboard flow
- validation: unable to run runtime/api-server typecheck/tests in this clone because local tsc/tsx dependencies are not installed
* fix(desktop): dashboard v2 polish — hook order, debounce, style audit

Bug fixes (correctness):
- TableView: move useCallback / useRef / useState / useEffect above
  early returns so hook order is stable when rows or columns are empty
- ChartPanel: same fix for both Cartesian and Pie body — useMemo
  was running after xIdx / labelIdx early returns
- BoardView: replace render-phase setActiveGroupBy with useEffect
- DashboardRenderer: debounce content (250ms) so editor keystrokes
  don't reparse + refire every panel's SQL on each character

Style polish (Cursor / Notion direction):
- StatGridPanel: drop card chrome (rounded-xl bg-card shadow-md),
  render as hairline-divided KPI row; title becomes a small uppercase
  section label rather than a card heading
- KpiCard: text-3xl/2xl font-semibold for values (was font-medium);
  loading replaces 9-cell hb-spinner with a single shimmer rect;
  progress bar drops bg-foreground/85 opacity hack
- ChartPanel / DataViewPanel: header description moves from inline
  bullet to a second line under the title; body padding unified to
  px-4 py-3 (was py-3/py-4/pb-3 across panels)
- StatusBadge: rounded-full → rounded-md to match colors-map chips
- TextPanel: prose-p:text-foreground/85 → text-foreground (no opacity
  on tokens)
- Empty state: new shared EmptyState component with per-panel-type
  Lucide icon (Table2 / Columns3 / Rows3 / chart-kind) + unified
  copy "Nothing here yet."

* feat(desktop): chart polish — fix stacked-bar radius, add color_by_sign

- ChartPanel: stacked bars now use radius=0 instead of [3,3,0,0];
  per-segment rounded tops looked like dents in the column
- ChartSpec: new optional `color_by_sign` flag for single-series bar
  charts. Each row's bar uses palette[0] (sky) for ≥0 values, palette[1]
  (orange) for negatives — Cells injected as Bar children
- Schema parser accepts `color_by_sign: true` and ignores it on
  multi-series / line / area where it doesn't apply

The intended consumer is signed-metric charts like P&L-by-ticker:
one column of pnl, sky/orange split by sign — much cleaner than the
two-series gain/loss workaround.

* feat(desktop): dashboard refresh feedback, auto-refresh, view-state persistence

- Persist table sort + column widths and board group-by to localStorage,
  keyed by dashboard absolutePath. usePersistedState hook degrades to
  plain useState when no key is provided (ephemeral previews)
- Refresh indicator pill in dashboard header: "Refreshing…" while any
  panel query is in flight, then "Updated 5s ago" with a 30s tick
- Schema: top-level refresh_interval (seconds, ≥10). DashboardBody sets
  up setInterval that calls bumpDashboardRefreshKey
- Theme-subscribed chart palette: useSeriesPalette hook watches html
  classList for "dark" via MutationObserver, so charts repaint on theme
  toggle without a manual refresh
- ErrorMessage component: SQL errors are now click-to-expand instead
  of single-line truncate-with-title — used in KpiCard / ChartPanel /
  DataViewPanel
- Currency column default width 110 → 130 (tight on $1,234.56)

* fix(desktop): KPI delta error visible + BoardView card affordance

- KpiCard: when delta_query fails, render a small AlertTriangle badge
  in the delta slot (was silently hidden — looked like no delta_query
  was configured)
- BoardView card: drop tabIndex={0} (no click handler — focus ring +
  hover lift on a non-interactive element is a fake affordance);
  swap transition-all + hover:shadow-sm for transition-colors with
  bg / border-only hover, per design rules

* fix(desktop): dashboard polish — sort, row paging, persistence write, etc.

- StatGridPanel: divide-border/50 → divide-border (no opacity hack on
  token; renders properly in dark mode)
- Description spacing: panels mt-0.5 → mt-1 to match dashboard header
- TableView: sort cycles asc ↔ desc only (was asc → desc → null which
  was reverse-discoverable); update title hint accordingly
- TableView + ListView: progressive row loading. Default still 500;
  "Show 500 more" button extends in chunks until all rows visible
- DataViewPanel: max-h 560 → 800 in fullWidth mode (long tables had
  no breathing room when the dashboard expands to fill the pane)
- usePersistedState: 200ms debounce on localStorage writes — column
  resize fires setItem on every frame otherwise
- configure Electron WebAuthn on macOS from packaged desktop config so embedded browser tabs can surface platform passkey prompts
- emit a packaged macOS WebAuthn keychain access group from build-time config using APPLE_TEAM_ID or an explicit override
- add the matching keychain access-group entitlement required for signed macOS builds
- add a source-level regression test covering the packaged-config, main-process, and entitlement wiring
- validate with node --test desktop/electron/macos-webauthn.test.mjs and npm --prefix desktop run typecheck
- add GPT-5.5 to the OpenAI Codex local model catalog and seeded provider defaults
- update Codex OAuth runtime config seeding to include GPT-5.5 alongside existing GPT-5 models
- add Codex-specific model routing budgets for GPT-5.5 and keep API routing budgets aligned with official OpenAI limits
- extend desktop source-based tests to cover the new GPT-5.5 catalog entry and Codex routing behavior
- validation: npm --prefix desktop run typecheck
- validation: node --test --test-name-pattern='desktop codex wiring includes GPT-5.5 defaults and Codex-specific routing budgets|desktop model catalog carries reasoning metadata and the composer persists thinking preferences' desktop/electron/runtime-provider-models.test.mjs
- note: desktop/src/components/auth/AuthPanel.test.mjs has pre-existing unrelated failures on this branch, so only the new targeted assertions were run
- update AuthPanel and BillingSummaryCard source assertions to match the current compact provider UI and account layout
- make multiline copy and JSX structure checks whitespace-tolerant so the tests stop failing on formatting-only drift
- keep the scope to the test file only and leave the unrelated desktop/package-lock.json change out of the commit
- validation: node --test desktop/src/components/auth/AuthPanel.test.mjs
- validation: npm --prefix desktop run typecheck
- add runtime.subagents.model parsing, persistence, and a shared subagent execution model resolver
- route delegated, resumed, continued, cron, and task-proposal subagents through the fixed global subagent model
- add a desktop Defaults selector and Electron config bridge support for the subagent model setting
- document the setting and add runtime plus desktop coverage for the new subagent execution path
- validation: npm run typecheck (desktop); node --test desktop/electron/runtime-config-binding-sync.test.mjs desktop/src/components/auth/AuthPanel.test.mjs; npm ci in runtime/api-server and runtime/state-store; node --import tsx --test src/subagent-model.test.ts src/runtime-config.test.ts src/runtime-config-cli.test.ts src/runtime-agent-tools.test.ts src/cron-worker.test.ts src/app.test.ts; npm run typecheck (runtime/api-server)
- let subagent execution fall back to the current composer-selected model when `runtime.subagents.model` is unset
- thread the current selected model through delegated task, resume, continue, and task-proposal acceptance flows instead of reusing the model captured when the child session started
- add a `Follow composer` subagent-model option in desktop settings and remove the automatic default-model seeding so an unset override stays truly unset
- update runtime tests, desktop source-shape checks, and model-configuration docs for the new fallback behavior
- validation:
  - `npm run typecheck` in `desktop`
  - `node --test src/components/auth/AuthPanel.test.mjs electron/runtime-config-binding-sync.test.mjs` in `desktop`
  - `node --test --test-name-pattern="accepting a task proposal starts background work without surfacing a hidden session id|app shell requests remote task proposal generation without a separate success banner" src/components/layout/AppShell.test.mjs` in `desktop`
  - `npm run typecheck` in `runtime/api-server`
  - `node --import tsx --test src/subagent-model.test.ts src/runtime-agent-tools.test.ts src/app.test.ts src/cron-worker.test.ts src/runtime-config.test.ts src/runtime-config-cli.test.ts` in `runtime/api-server`
jeffreyliimerch and others added 27 commits May 28, 2026 15:34
- stage the repo-level shared contract tree into the packaged runtime root so staged api-server builds can resolve onboarding-contract imports
- include shared/ in desktop runtime bundle stale detection and the macOS runtime packager cache key so contract edits trigger rebuilds
- add focused regression coverage for runtime bundle source inputs and local runtime packaging expectations
- validation: node --test apps/desktop/scripts/runtime-bundle-state.test.mjs
- validation: node --test --test-name-pattern="build_runtime_root stages package-local scripts before dependency installs|package_macos_runtime cache key includes shared contract inputs|package_macos_runtime.sh bundles local node and python runtimes and exports them" runtime/deploy/package_runtime_node_bundle.test.mjs
- validation: npm run prepare:runtime:local
- validation: bun dev
…r in fullscreen (holaboss-ai#403)

* fix(onboarding): apply chat-focus main-view choice on freshly-created workspace

Picking "chat" for a new workspace didn't take effect when the previous
workspace had any tabs open: the seed effect that flips focusMode locks
itself to the new workspace id the moment selectedWorkspaceId changes,
so a same-tick map write was read as undefined and skipped. And the
auto-exit-focus effect compared the new workspace's tab count against
the *previous* workspace's count, clobbering the freshly-set choice
back to split as soon as the new tab list hydrated.

- workspaceDesktop.createWorkspace now sets focusMode synchronously
  alongside the map write, so creation no longer depends on the seed.
- NewAppShell re-baselines prevTotalTabsRef on workspace switch so
  cross-workspace tab-count deltas can't trip auto-exit.

* fix(new-shell): collapse macOS stoplight gutter when window is fullscreen

The sidebar header reserved an 80px left gutter on macOS to clear the
traffic-light buttons, but kept reserving it in fullscreen where the
buttons are hidden — pushing the workspace switcher off-center and
wasting the top-left strip.

- StoplightContext now subscribes to the existing ui.onWindowStateChange
  feed and returns false in fullscreen, so every consumer (including
  the legacy AppShell that already uses this hook) collapses the gutter
  at the right time.
- NewAppShell wires the new-shell tree into StoplightProvider so the
  sidebar's WorkspaceSwitcher can consume the hook instead of branching
  on platform inline.

---------

Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
* feat: upgrade pi read and edit tools to hashline patches

- add repo-local hashline read/edit tool overrides for Pi harness sessions instead of patching the upstream pi-coding-agent package directly
- change read to emit snapshot-tagged numbered output so follow-up edits can anchor to the exact file view the model saw
- change edit to accept single-string hashline patch input, preflight multi-file batches, reject stale tags, and return the next snapshot header after writes
- extend workspace boundary enforcement to inspect hashline section headers inside edit.input so the new payload format cannot bypass local path restrictions
- add focused harness-host and workspace-boundary regression tests covering snapshot reads, anchored edits, stale-tag rejection, and multi-file preflight behavior
- validation: npm run typecheck
- validation: node --import ./node_modules/tsx/dist/loader.mjs --test src/pi-hashline-tools.test.ts
- validation: node --import ./node_modules/tsx/dist/loader.mjs --test ../harnesses/src/workspace-boundary.test.ts
- validation: npm test

* fix(runtime): propagate operator timezone through cronjobs and prompts

- persist runtime user profile timezone through control-plane storage, Electron auth fallback, and renderer typings
- include operator timezone in agent prompt context and ts-runner current-user hydration so relative dates resolve against the user profile
- pin cronjob metadata to a resolved timezone and compute next-run scheduling from the runtime user timezone instead of the host default
- update regression coverage for runtime profile persistence, cronjob scheduling, ts-runner hydration, agent prompt/config context, and raw cronjob routes
- validation:
  - `bun --filter=@holaboss/runtime-state-store run typecheck`
  - `bun --filter=@holaboss/runtime-api-server run typecheck`
  - `bun --filter=holaboss-local run typecheck`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runtime user profile round trip preserves manual value and auth fallback only fills when empty" src/store.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="cronjob helpers honor next_run_at and preserve legacy scheduling fallback" src/cron-worker.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="composeBaseAgentPrompt includes current user context when provided" src/agent-runtime-prompt.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="projectAgentRuntimeConfig includes current user context as a context message" src/agent-runtime-config.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runTsRunnerCli loads current user context from the runtime profile" src/ts-runner.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runtime tools capability routes expose local onboarding and cronjob actions|cronjobs and session state routes preserve local payload shape" src/app.test.ts`
  - `node --test ../apps/desktop/electron/control-plane-owned-state.test.mjs`

* feat: streamline teammate creation and dashboard cards

- replace direct teammate creation in the teammates pane with a lightweight dialog that collects name and role
- route teammate creation requests through the ensured main session so the coordinator can follow the create-teammate skill and ask follow-up questions in chat
- remove duplicated success-rate cards from the workspace dashboard and add a waiting-for-review summary card
- update workspace surface tests to cover the new teammate creation flow and revised dashboard copy
- validation: bun test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs
- validation: npm --prefix apps/desktop run typecheck

* feat: add built-in HR teammate and remove preferred tool routing

- remove teammate preferred tools from the desktop UI, API payloads, runtime capability schemas, routing records, and persisted capability-profile model
- seed a built-in HR system teammate for every workspace alongside General and keep both system teammates normalized on read/write paths
- update teammate creation prompts and the embedded create-teammate skill to treat teammate requests as production bootstrap work with integration checks and teammate-local skills when needed
- adjust teammate routing and tests to rely on capabilities, summaries, instructions, and skills instead of preferred-tool matching
- refresh desktop and runtime tests for the HR teammate roster and the simplified capability profile shape
- validation: bun test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs
- validation: npm --prefix apps/desktop run typecheck
- validation: bun run typecheck (runtime/state-store)
- validation: bun run typecheck (runtime/api-server)
- validation: bun test runtime/api-server/src/teammate-routing.test.ts
- validation: bun test runtime/api-server/src/teammate-skill-files.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-config.test.ts
- note: DB-backed runtime test suites remain blocked in this shell by the existing better-sqlite3 native module/runtime mismatch

* fix: route hashline edit activity through task followups

- make hashline edit parsing tolerate unified-diff style body rows and retro-strip context prefixes within a hunk
- parse hashline `¶path#TAG` edit headers in the desktop chat trace so edit calls resync the edited file display and show file-focused activity details
- record edited file paths in runtime tool usage summaries and carry them into subagent/main-session follow-up payloads so edit-only tasks materialize concrete user-facing updates
- add regression coverage for hashline parsing, chat-pane file sync, turn summaries, and main-session event prompt payloads
- validation: `node --import ./node_modules/tsx/dist/loader.mjs --test src/pi-hashline-tools.test.ts`, `node --test --test-name-pattern="chat pane syncs the shared file display from live file-oriented tool calls" apps/desktop/src/components/panes/ChatPane.test.mjs`, `node --import tsx --test --test-name-pattern="compactTurnSummary surfaces edited files when a run completed without assistant text|queued main-session event prompt entry preserves edited file paths for follow-up routing" src/turn-result-summary.test.ts src/main-session-event-prompt.test.ts`, `npm run typecheck`

* feat: tighten teammate delegation and streamline onboarding surfaces

- require explicit teammate ids for delegate_task across tool schemas, client normalization, prompt guidance, and runtime validation so managers choose assignees intentionally
- render teammate roster entries with canonical ids to prevent name-vs-id delegation failures like the U4 General/general mismatch
- shift onboarding flows and tests away from lab-backed wording toward source-controller onboarding and updated workspace language
- simplify the issue detail surface by removing mutable properties controls from the sidebar and aligning related desktop test expectations
- add the stop-slop embedded skill bundle for stricter teammate output quality guidance
- validation: bun test runtime/harness-host/src/pi-runtime-tools.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts
- validation: bun test runtime/harnesses/src/capability-http.test.ts --test-name-pattern "runtime delegate-task client forwards use_user_browser_surface when explicitly requested"
- validation: bun test runtime/harness-host/src/pi-runtime-tools.test.ts --test-name-pattern "Pi runtime subagent tools normalize delegated task bodies and control routes"
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts --test-name-pattern "teammate routing context"
- note: bun test runtime/api-server/src/runtime-agent-tools.test.ts remains blocked here because Bun does not support better-sqlite3 in this environment
- note: bun run typecheck in runtime/api-server remains blocked by existing unrelated ONBOARDING_ALIGNMENT_STATE errors in src/app.test.ts

* fix(main-session): route issue creation through composer

- route the new-shell `New issue` sidebar action through the chat composer prefill flow with `New issue: ` instead of opening the modal directly
- remove `teammates_create` and `teammate_skills_create` from the main-session runtime tool allowlist while leaving delegated/subagent behavior unchanged
- update new-shell sidebar source tests and the focused ts-runner allowlist assertion to match the revised main-session surface
- validation:
  - `node --test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs`
  - `node --test apps/desktop/src/components/layout/new-shell/NewIssueDialog.test.mjs`
  - `node --test apps/desktop/src/components/layout/new-shell/IssuesSidebar.test.mjs`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runTsRunnerCli keeps main workspace sessions on a coordinator surface" src/ts-runner.test.ts`
  - `bun --filter=@holaboss/runtime-api-server run typecheck`

* feat: refine direct workspace onboarding flow

- expand the workspace onboarding alignment report contract to cover user intent, work context, research basis, integrations, teammates, workspace rules, apps, teammate-owned cronjobs, and implementation notes
- update onboarding report sanitization, review UI labels, and onboarding prompt expectations to match the v2 report shape
- keep workspace_onboarding implementation in the same session by excluding delegated task tools from the onboarding capability surface and rejecting delegation at runtime
- update onboarding runtime tests for the v2 report payload and for zero background tasks in direct workspace onboarding
- Validation: bun --filter=@holaboss/runtime-api-server run typecheck
- Validation: node --import tsx --test --test-force-exit src/agent-capability-registry.test.ts src/agent-runtime-prompt.test.ts

* feat: restrict teammate bootstrap to HR sessions

- gate the embedded create-teammate skill and teammate bootstrap runtime tools so only HR-owned sessions can see or invoke them
- route front-session teammate bootstrap requests through HR, tighten prompt guidance, and enforce the restriction in the API/runtime service layer
- add delegated task continuation via reply_task, return stable task_id payloads to managers, and stop exposing top-level subagent_id in delegate_task responses
- preserve or draft chat prefills intentionally for issue and automation flows, and make embedded-skill markdown changes invalidate the staged desktop runtime bundle
- extend desktop and runtime tests for skill visibility, runtime projection, task follow-up routing, and chat prefill behavior
- validation: node --test apps/desktop/src/components/layout/new-shell/ChatPanel.prefill.test.mjs
- validation: node --test apps/desktop/scripts/runtime-bundle-state.test.mjs
- validation: node --import tsx --test src/workspace-skills.test.ts
- validation: node --import tsx --test --test-name-pattern "composeBaseAgentPrompt includes teammate routing context when provided" src/agent-runtime-prompt.test.ts
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli exposes teammate bootstrap runtime tools only to the HR subagent|runTsRunnerCli includes teammate-local skills for assigned subagent runs" src/ts-runner.test.ts

* feat: refine onboarding orchestration and specialist routing

- evolve workspace onboarding toward the new alignment-report and implementation-orchestration flow, including direct source-workspace onboarding sessions and phase-aware execution rules
- thread onboarding state through runtime config, capability manifests, and ts-runner projection so alignment hides implementation-only tools while implementation can delegate work
- add teammate roster listing plus HR/App Builder session guards for teammate provisioning and managed app runtime tools
- introduce the App Builder system teammate and update routing, workspace skills, and embedded tool guidance so app work routes away from General
- pass session ids through runtime app capability routes and expand state-store system teammate ordering/backfill behavior for the specialist roster
- update prompt, runner, runtime-tool, state-store, and routing tests to cover onboarding phases, specialist capability surfaces, and delegated execution behavior
- Validation:
  - bun --filter=@holaboss/runtime-api-server run typecheck
  - node --import tsx --test --test-name-pattern="workspace-onboarding" src/agent-capability-registry.test.ts src/ts-runner.test.ts

* fix: restore HR teammate skill loading in packaged runtime

- resolve HR delegated-run skill scoping from the assigned issue or subagent record before falling back to session context
- stop the PI harness from auto-injecting every embedded skill and honor only explicitly projected skill directories plus workspace-local skills
- allow workspace-boundary reads inside approved external skill directories so bundled runtime skills can be loaded without opening arbitrary paths
- fix the embedded create-teammate skill frontmatter so the real HR skill is discoverable by the workspace skill loader
- simplify the manual teammate-creation prompt to route directly through the built-in HR teammate
- add regressions for stale teammate context, real embedded HR skill discovery, PI skill-dir projection, and workspace-boundary external skill access
- restage the local macOS runtime bundle so the desktop app can pick up the fixed embedded skill
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0
-
- bun test v1.3.11 (af24e281)
- ✔ workspace surfaces wire board and dashboard tabs through the shell (5.117125ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 45.816458

* fix: align runtime api tests with merged cronjob behavior

- update cronjob route and workspace-lab expectations to include the pinned timezone metadata now attached by the runtime
- pin the cron worker next_run_at test to UTC so nextRunAt remains authoritative for the due-check path under the current scheduler logic
- update delegated task assertions to read the latest subagent id from the stored child-session run instead of assuming a latest_run payload on delegateTask manager responses
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0

* test: align runtime api server expectations with merged capability context

- include the now-present  field in capability-manifest context assertions for subagent and main-session tests
- update delegated task route fixtures to provide the required  on capability-route requests
- assert the parsed skill route  payload instead of matching against JSON-escaped response text
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0
-

* feat: add optional sub-issues across runtime and desktop

- add optional parent-child issue relationships to the state store, runtime API, and delegated task payloads
- add desktop related-issues UI and inline sub-issue creation while keeping top-level issues valid
- expose parent issue metadata through Electron bridge and runtime capability clients
- fix workspace runtime DB bootstrap so older issue tables migrate parent_issue_id before parent-based indexes are created
- update store and desktop surface coverage for the new issue hierarchy behavior
- validation: npm --prefix runtime/state-store run typecheck
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix apps/desktop run typecheck
- validation: node --test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs apps/desktop/src/components/layout/new-shell/NewIssueDialog.test.mjs
- validation: node apps/desktop/scripts/ensure-runtime-bundle.mjs

* fix: hydrate delegated task payloads in runtime API responses

- hydrate newly delegated tasks through getTask before returning from the runtime capability subagents route
- keep the runtime-agent-tools service contract unchanged while restoring the manager-facing HTTP payload shape with latest_run details
- fixes runtime-api-server CI assertions that expect delegated task responses to expose the full task record instead of a stripped run payload
- validation: npm --prefix runtime/api-server run typecheck

* fix: preserve delegated task issue_id alias

- restore issue_id on manager-facing delegated task payloads alongside task_id
- keep hydrated task responses backward compatible with runtime-api-server delegated task tests and existing consumers
- validation: npm --prefix runtime/api-server run typecheck

* fix: align delegated task route test with hydrated task payload

- update the runtime API delegated task test to assert issue fields at the top level and run metadata under latest_run
- keep child session lookups aligned with the hydrated task payload returned by the subagent creation endpoint
- validation: bun --filter=@holaboss/runtime-api-server run typecheck

* feat: expand hashline reads and promote ripgrep tooling

- expand Pi hashline read support to cover directory pagination, PDF extraction, DOCX extraction, and clearer binary-file handling\n- add regression coverage for the richer hashline read surfaces in the harness-host test suite\n- promote ripgrep to the primary projected search tool while keeping the legacy grep request key as a compatibility alias\n- update runtime tool manifests, default tool sets, and workspace boundary policy to recognize ripgrep as a first-class local inspect tool\n- keep glob exposure unchanged and retarget the PI host/tool-projection path so runtime sessions now see ripgrep instead of grep\n- Validation: bun test runtime/harness-host/src/pi-hashline-tools.test.ts; bun test runtime/harness-host/src/pi.test.ts --test-name-pattern "filterPiToolDefinitionsForRequest"; bun test runtime/api-server/src/ts-runner.test.ts --test-name-pattern "runTsRunnerCli keeps main workspace sessions on a coordinator surface|runTsRunnerCli stages browser tools for subagent executor sessions and strips orchestration runtime tools"
- quote app-derived SQLite identifiers in the state backend so hyphenated app ids boot correctly
- add sqlite identifier regression coverage and restore SDK reference fixture apps used by the package test suite
- narrow fetch-like typing and soften MCP tool registration generics so the SDK package typechecks cleanly
- add a Bun source export plus local UI ambient declarations so file:-installed workspace apps resolve the SDK without a prebuilt dist
- sync the embedded app-builder skill sdk-package mirror with the SQLite and transport typing fixes
- Validation: bun run typecheck; bun test
- add a dedicated GitHub Actions workflow for notarized Intel macOS x64 desktop releases
- disable packaged mac updater metadata for the Intel build so shared latest-mac.yml and beta-mac.yml remain Apple Silicon only
- add a regression test covering the Intel workflow contract and builder config toggle
- document the separate Intel macOS release path in the desktop release README
- validation: `node --test apps/desktop/electron/intel-macos-release-workflow.test.mjs`
… files in @ picker (holaboss-ai#407)

* fix(new-shell): reset internal file tabs on workspace switch

Internal (file/image preview) tabs live in a global jotai atom, but
nothing was clearing them when selectedWorkspaceId changed. Switching to
or creating a new workspace inherited the previous workspace's open file
tabs in TopChrome, with no way to scope them away.

Add a useEffect in NewAppShellContent that resets internalTabsAtom and
activeInternalTabIdAtom on every workspace switch. Browser tabs already
re-fetch via useWorkspaceBrowser's own reset, so this closes the gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(new-shell): surface recent files in chat composer @ picker

Typing @ in the composer now lists recently-opened files first
(most-recent-first) before the alphabetical workspace tree, so users
don't have to retype paths for files they just had open.

Recent files outside listWorkspaceFiles' bounded walk also surface:
the walk caps at maxDepth=4 / maxFiles=500 and skips dotdirs
(.holaboss, .git, node_modules, dist, build, out, .next, .cache,
.turbo). Before this change the @ picker only ranked recents that
already lived in workspaceFiles, so a recent in .holaboss/notes/foo.md
silently disappeared from suggestions and had to be hand-typed.

- recentFileEntriesForWorkspace builds a WorkspaceFileEntry-shaped row
  per recent file in the current workspace. Prefer the walk's rich
  entry when present; otherwise synthesize one from
  selectedWorkspace.workspace_path + recent.filePath, falling back to
  recent.label when no workspace_path is available.
- composerMentionableItems iterates that array first, then falls
  through to the alphabetical workspaceFiles list with a dedupe set.
- mentionableFilesByHandle also includes synthesized entries so
  send-time stageSessionAttachmentPaths can resolve them by absolute
  path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ge (holaboss-ai#409)

The TeammatesPane was a snapshot of the older Card-heavy shell design —
multiple stacked `<Card bg-background/55>` wrappers, opacity-hacked
`text-foreground/N` colors, arbitrary tracking-[0.16em] eyebrows,
chunky `h-9 rounded-xl` badges, mismatched tab heights, and tabs whose
active state was invisible. Reworks the surface to use the same token
language as the dashboard, board, and issue detail.

Top bar and detail action row
- Top bar collapses to the standard 12-row eyebrow header (matches
  dashboard / board / issue detail) and surfaces `n of m` on the right
  in list mode, `Teammates / Name` breadcrumb in detail mode.
- Detail action row from a multi-line h-9 chunky strip with three full
  Badge components → one tight 8-row line with inline `● Status ·
  4 skills · 3 issues` text and icon-only Refresh / Archive controls.
  Save stays as the only primary CTA.

List view
- Drop the outer `Card bg-card/80 shadow-sm` wrapper. Toolbar and table
  are independent layers on a `bg-fg-4` page wash.
- Search input shrunk to h-8. "Active" / "Archived" filter pill chips
  removed in favour of a single Show / Hide archived toggle.
- Table itself: `rounded-xl border-border bg-card` with a tinted
  `bg-fg-2` header row + `divide-y divide-border` body. Row height drops
  from py-4 to py-2.5. Avatar chip shrinks from `size-10 rounded-2xl
  border` to `size-7 rounded-full bg-fg-8`. Text hierarchy goes from
  text-[15px]/text-[13px] with opacity hacks to `text-sm` / `text-xs`
  with semantic muted-foreground / foreground.

Detail sidebar
- Avatar chip from `size-14 rounded-3xl` to `size-10 rounded-full bg-fg-8`.
- Name title from `text-2xl` to `text-base font-semibold` (detail pages
  don't need a hero number; the breadcrumb already carries identity).
- Two stacked Badge components for status + kind → one inline
  `● Active · System` line.
- Routing note from a full Card with header to a plain eyebrow + body
  panel.
- MetricRow text hierarchy goes from `text-foreground/45` /
  `text-foreground/82` opacity hacks to `text-muted-foreground` /
  `text-foreground` tokens with tabular-nums.

Tab strip
- TabsTrigger sizing unified at h-9 (Skills was at h-12).
- Active-state visibility, which the variant="line" pseudo-element
  approach was failing to render. Now uses `-mb-px` to overlap the
  container's hairline border and toggles only `border-b-foreground`
  via `data-active:!border-b-foreground` — the top / left / right
  edges stay at width 0 so the active foreground colour can't leak
  into a faux box. Inactive triggers use border-b-transparent so the
  container hairline shows through.

Tab content
- Activity / Issues content reshapes from `<Card bg-background/55>` with
  `<button rounded-2xl border bg-background/70 px-4 py-3>` rows into
  `DetailSection` eyebrow + `<ul divide-y divide-border>` line lists,
  matching the dashboard Recent activity / Recent issues pattern.
- Issues panel drops the duplicate Status + priority badges in the row,
  keeps only the inline priority pill + status word + relative time.
- Instructions panel migrates 4 ad-hoc `text-xs tracking-[0.16em]
  text-foreground/42` field labels to a shared FormField helper. Drops
  `bg-background/75` input overrides.
- Skills panel migrates from `<Card bg-background/55>` to DetailSection
  with an inline `+ Add skill` action slot. Each skill is a two-tone
  card: `bg-fg-2` header strip with the skill id + Reveal / Remove
  controls, `bg-card` body holding the FormField inputs. Path hint is
  now a single mono line, dropping the standalone `rounded-2xl bg-bg/45
  text-foreground/55` info panel.

New teammate dialog
- The two form fields adopt FormField, error banner switches from
  `border-border bg-card text-foreground/65` to the proper
  `border-destructive/30 bg-destructive/10 text-destructive` semantics.

Local helpers (added at file bottom)
- DetailSection({ eyebrow, meta?, description?, action?, children })
- FormField({ label, hint?, children })
- EmptyRow({ label })
- issueRowDotVariant(status) → success | primary | warning | info | muted

Cleanup
- All `text-foreground/{45,48,55,58,65,68,82}` opacity hacks gone.
- All `tracking-[0.16em] | 0.22em | 0.18em` arbitrary tracking values
  gone; replaced with the standard tracking-wider for uppercase
  eyebrows.
- All `text-[10px] | 11px | 13px | 15px` arbitrary font sizes gone;
  replaced with the token xs / sm / base / 4xl scale.
- `Badge` / `Card` / `CardAction` / `CardContent` / `CardDescription`
  / `CardHeader` / `CardTitle` imports removed (no longer used).
- `ReactNode` type added to react imports for the new helpers.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lish) (holaboss-ai#410)

Phase 1 of the GitHub → R2 migration. After the `Publish validated
GitHub release` step pushes the artifacts to holaboss-ai/holaOS-releases,
a new `Mirror release assets to Cloudflare R2` step copies the same
binaries + manifests to an R2 bucket via the S3 API (aws s3 cp). Goal:
let us validate global download throughput from R2 before flipping
electron-updater's `provider` away from `"github"` in a later phase.

GitHub Releases stay authoritative — installed clients ignore R2
entirely until a future release ships a new electron-builder.config.cjs
with `provider: "generic"` pointing at the R2 (or fronting custom)
domain.

How the new step works
- Re-discovers assets under `release-assets/` with `find` instead of
  inheriting the previous step's bash array, so the mirror is
  self-contained and safe to re-run in isolation.
- Pushes every dmg / zip / exe / tar.gz / sha256 / blockmap / yml /
  json file straight to the bucket root, keeping the same flat layout
  electron-updater's `generic` provider expects.
- Cache-Control: manifests + sha256 get `max-age=60 must-revalidate`
  so the updater always picks up new versions promptly; binaries
  (version is baked into the filename) get `max-age=31536000 immutable`.

Soft-gated
- If any of `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` /
  `R2_ENDPOINT_URL` / `R2_BUCKET` secrets are missing the step logs
  a `::warning::` and exits 0. That keeps this PR safe to merge before
  the bucket + GH secrets are provisioned — the first release after
  secrets land starts mirroring automatically with no further code
  change.

Required GitHub secrets to enable mirroring
- `R2_ACCESS_KEY_ID` — R2 access key
- `R2_SECRET_ACCESS_KEY` — R2 secret access key
- `R2_ENDPOINT_URL`     — e.g. https://<account-id>.r2.cloudflarestorage.com
- `R2_BUCKET`           — bucket name, e.g. `holaos-releases`

`AWS_DEFAULT_REGION=auto` is hardcoded in the step (R2 ignores region
but the AWS SDK requires the variable to be set).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hide + close polish) (holaboss-ai#404)

* feat(new-shell): restore Control Center entry (sidebar footer + ⌘0)

NewAppShell never mounted WorkspaceControlCenter, so the cross-workspace
grid dashboard had no way in — clicking workspaces in the sidebar popover
just switched the active workspace, with no overview view.

- new controlCenterOpenAtom (jotai), wired into browserViewSuspendedAtom
  so the BrowserView detaches when the takeover is showing
- SidebarGlobalFooter: "All workspaces" NavItem above Settings; pairs the
  app-level entries together (both jump out of the current workspace)
- ⌘0 toggles the takeover; consistent with ⌘T / ⌘K / ⌘\\ in NewAppShell
- ControlCenterTakeover wraps WorkspaceControlCenter with minimal props;
  enter/select wire selectedWorkspaceId, create routes to the existing
  createWorkspaceOpenAtom. Drag-reorder, density, completion highlights
  are no-ops for v1 — they require lifting state out of legacy AppShell
  and are tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(new-shell): hide sidebar in CC takeover + cache snapshots across reopens

Follow-ups stacked onto the Control Center entry PR:

- Sidebar is hidden while CC is open; full-screen takeover replaces it.
  The footer entry is unreachable in that state, so add a top-left close
  button (X), an Esc handler, and keep ⌘0 as the keyboard exit.
- Add a module-level snapshot cache in WorkspaceControlCenter keyed by
  workspaceId, storing the last-seen { mainSession, messages,
  runtimeState, runtimeCardState, runningSubTasks }. WorkspaceCard
  initial state is hydrated from the cache when present, so reopening CC
  paints the same content immediately instead of flashing the "Loading"
  spinner before the same data comes back from disk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(new-shell): CC takeover offsets close button past macOS traffic lights + restores window drag

The previous absolute X at top-3 left-3 sat right on top of macOS's fixed
traffic lights (x:14 y:16). Also, hiding the sidebar + Sidebar/TopChrome
during CC takeover stripped the only window-drag region, so the
frameless window couldn't be moved while CC was open.

Replace the absolute button with a thin h-10 header row at the top of
the takeover that is itself window-drag, with the close button marked
window-no-drag and inset by STOPLIGHT_PAD_PX on darwin (12px elsewhere).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cleanup (holaboss-ai#408)

* fix(integrations): polish OAuth UX and route errors through toast

- Add "Reopen" affordance + earlier hints in OAuthWaitIndicator
- Replace inline statusMessage with persistent sonner toasts so errors
  aren't silently overwritten by follow-up mutations
- Disable chat composer while an integration OAuth is mid-flight

* feat(integrations): real-time Composio invalidation via main-process SSE bridge

Subscribes from electron main to the cloud BFF's connected_account event
stream, forwards invalidation frames to renderer over IPC, and races them
against the OAuth poll loop so newly-connected accounts surface as soon
as the webhook fires instead of waiting for the next focus tick.

- Add composio-events-bridge in main (cookie-auth SSE, backoff, restart
  on auth rotation)
- Expose electronAPI.composio.{onConnectionInvalidated,onStatusChange}
- useIntegrationBinding refreshes only when the event matches a rendered
  account_external_id
- Mark sonner toaster region as no-drag so close buttons aren't swallowed
  by the macOS title-bar drag region

* fix(runtime): isolate failures in by-workspace/runtime-states aggregation

Wrap getActiveWorkspaceLab, listRuntimeStates, and per-record payload
construction in try/catch so one moved or deleted workspace bundle no
longer 500s the whole endpoint — log and skip the bad source instead.

* docs: add remote-workspace and workspace-as-an-app design explorations

Two parallel PM/engineering proposal sets with shared structure: PM
perspective, proposal flow, user scenarios, followup conclusions, plus
an engineering review for remote-workspace.

* fix(integrations): use runtime connection_id after composioFinalize

composioFinalize writes a runtime row whose connection_id is a fresh
UUID; downstream callers (setWorkspaceDefaultAccount, rebindWorkspaceAppsForProvider,
MCP host) key off THAT id, not Composio's ca_xxx. Passing the upstream
ca_xxx through to those callers produced 400 "integration connection
ca_xxx not found" failures during the auto-default + rebind sweep that
runs right after the user finishes connecting from Settings.

Capture the return value of composioFinalize and route runtimeConnectionId
through to both setWorkspaceDefaultAccount and rebindWorkspaceAppsForProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(integrations): detect abandoned OAuth and surface Reconnect affordance

Composio does not emit any webhook for connected_accounts that stay in
INITIATED — `connected_account.expired` only fires for ACTIVE tokens that
later expire, verified empirically. That means the only way to learn the
user closed the OAuth tab is to infer it from desktop-side signals.

Add a focus-based heuristic in the connect poll loop:
  * track the first time the desktop window regains focus after the
    OAuth tab opens (firstFocusBackAt)
  * once we've been focused for OAUTH_ABANDON_FOCUS_GRACE_MS (12s) AND
    total elapsed exceeds OAUTH_ABANDON_MIN_ELAPSED_MS (15s) AND
    status is still INITIATED, throw IntegrationConnectAbandoned
  * thresholds tilt toward false negatives (slow to abandon) over
    false positives — the recovery cost is asymmetric: a missed
    abandon costs 90s of wait, a false positive costs a full OAuth redo

Tighten COMPOSIO_POLL_MAX_TICKS from 100 to 30 (5 min → 90 s). The
heuristic above now gives sub-20s feedback for the common "closed the
tab" case, so a long absolute timeout no longer earns its keep.

UI side (useIntegrationBinding): add an errorKind discriminator so
the connect surface can tell "abandoned" apart from "generic" and
render a Reconnect button + plain-English copy instead of a banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(settings): drop redundant 'Back to app' rail button

Settings has two close affordances — the rail "Back to app" button at
the top of the left rail and the standard overlay-dismiss paths
(escape, click-outside, the workspace sidebar entries). The rail
button burns 40+ px of vertical space and visually competes with the
section list for first glance. Both shells already dismiss settings
through the overlay/state machine, so the button is pure repetition.

- SettingsScreen drops the button, ChevronLeft import, and the
  onBackToApp prop; rail top padding tightens to match.
- SettingsScreenRoot stops forwarding onBackToApp.
- AppShell + new-shell/Overlays stop wiring it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
…olaboss-ai#411)

* fix(agent): unblock auto-polish — retry, MCP URL, signature, polling

Four fixes to the post-build polish pipeline observed failing on
workspace 948762b3's twitter-engagement-tracker:

1. Pi retry tear-down (runtime/harness-host/src/pi.ts)
   The event mapper emitted run_failed on the first error message_end,
   tearing down the harness before pi-coding-agent's setTimeout-
   scheduled retry continuation could execute. Result: transient
   "openai/gpt-5.4: terminated" stream failures killed entire polish
   runs even though pi's _isRetryableError would have recovered.
   Now defer run_failed for errors matching pi's retryable regex;
   promote to terminal only on auto_retry_end success=false (retries
   exhausted) or agent_end with the failure still pending. Also
   await session.waitForRetry() after sendUserMessage. 4 new tests.

2. MCP SSE endpoint URL (runtime/api-server/src/runtime-agent-tools.ts)
   Scaffold template emitted JSON.stringify({sessionId, messagePath})
   as the "endpoint" event data. The MCP SSE protocol requires a URL
   string; the JSON object got URL-encoded as a path segment, hit
   404, and silently disabled every app's MCP tools. Replaced with
   /mcp/messages?sessionId=<id>.

3. Polish signature manifest (runtime/api-server/src/runtime-agent-tools.ts)
   Polish runs converged to the same editorial-newspaper signature
   per app because interface-design's exploration has no cross-app
   visibility. Polish prompt now reads sibling .signature.json files,
   injects them as a divergence constraint (must differ on >=2 of 5
   axes: typography / palette / layout_archetype / hero_treatment /
   density), and instructs the agent to write its own manifest at
   completion. v1 — agent-self-reported labels, no vision audit.

4. IPC coalesce for listRuntimeStates (apps/desktop/electron/main.ts)
   10+ React polling sites each ran their own setInterval against
   /runtime-states (~3.8 hits/sec sustained across workspaces).
   Added per-workspace in-flight Promise sharing + 500ms result
   cache at the IPC entrypoint. Long-term SSE push is not in scope.

Verified end-to-end on workspace 08c46a57 twitter-engagement: polish
completed in 12 min (162 tool_calls), .signature.json written, zero
mcp_server_unavailable events, zero retry tear-downs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): orient polish around user-doing-work, not app-introducing-itself

Polish runs keep producing magazine-cover / portfolio-page output even
with all the operational steps followed correctly. The cause isn't a
missing rule — it's a missing frame. The agent treats the dashboard as
a thing to describe (eyebrow → serif headline → subtitle prose →
SCREAMING_SNAKE_CASE labels lifted from the schema) instead of as a
tool the user opens to do work.

Add an ORIENTATION block at the top of the polish prompt, before
step 0. It re-positions the agent's attention from "showcase this app"
to "let the user work" and points at Linear's project view as the
neighborhood. No anti-pattern lists, no token rules — those would
anchor the agent on the failure mode (per the existing comment at
`maybeMapAssistantTerminalFailure`'s prior-iteration warning) and
the prompt is already long.

This is intentionally low-rule, high-direction. If subsequent polishes
still drift, the next levers are (a) real product references the
agent's prior already knows about, (b) a one-line critic question at
verify time — not more rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
- call the reusable publish-macos-intel-desktop workflow from manual CI releases
- require the Intel macOS artifact before the final publish-release job proceeds
- download and attach holaOS-macos-x64.dmg alongside the existing arm64 macOS assets
- update desktop release documentation to reflect the shared CI release flow and Intel rerun path
- extend release workflow policy assertions to cover the Intel macOS artifact wiring
- validation: node --test apps/desktop/electron/release-channel-policy.test.mjs apps/desktop/electron/intel-macos-release-workflow.test.mjs
- switch the Intel desktop release workflow from macos-13 to macos-15-intel
- keep the x64 runner guard in place while targeting GitHub's current hosted Intel image
- update the Intel workflow assertion to match the supported runner label
- validation: node --test apps/desktop/electron/intel-macos-release-workflow.test.mjs apps/desktop/electron/release-channel-policy.test.mjs
- pass HOLABOSS_ENABLE_APP_UPDATES=0 into the Intel macOS packaging steps that invoke electron-builder
- make electron-builder omit publish config and generated update files when app updates are explicitly disabled
- keep normal updater metadata generation unchanged for standard desktop release builds
- extend Intel macOS workflow assertions to cover updater-disable wiring in the packaging steps and builder config
- validation: node --test apps/desktop/electron/intel-macos-release-workflow.test.mjs apps/desktop/electron/release-channel-policy.test.mjs apps/desktop/electron/app-update-config.test.mjs
…olaboss-ai#412)

* fix(agent): give app_builder a neighborhood — Linear / Notion / Stripe

The polish prompt's ORIENTATION block (prior commit) only reaches the
auto-queued polish-pass path. Chat-triggered re-polishes go through
`delegate_task` to the app_builder teammate with a custom goal the
main agent writes — they never see the polish-pass prompt at all.

The shared chokepoint is `APP_BUILDER_TEAMMATE_INSTRUCTIONS` (the
teammate's persistent identity). Both polish paths run as this
teammate, so this is the right place for visual direction that needs
to land regardless of how polish was triggered.

Added two paragraphs:

1. Dashboard posture: tool the user opens to do work, not an
   introduction. Same direction as the polish prompt's ORIENTATION,
   restated in identity-level terms so it persists across all
   app_builder tasks (not just auto-queued polish).

2. Neighborhood: Linear's project view, Notion's database tables,
   Stripe's billing dashboard. Calm density — sparse meaningful
   color, depth from layered surfaces, variation by content type
   not by ornament. Specifically counters the "white card on white
   card, same treatment everywhere" output observed on the most
   recent re-polish where every metric, every section, every post
   used the identical card shape.

No anti-pattern lists (would anchor — see existing comment at
`maybeMapAssistantTerminalFailure`'s prior-iteration warning).
Positive direction + real product anchors only.

`ensureSystemTeammate` already detects instruction drift and updates
in place, so existing workspace DBs pick up the new text on next
runtime boot without manual migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(browser): full-page screenshot — verify the regions polish silently shipped

The agent verifies polish output via `browser_screenshot`, but Electron's
`webContents.capturePage()` only returns the visible viewport. On a
dashboard taller than 800px the lower sections — exactly where the
"narrative subtitles" and other less-polished content lives — are
invisible to the verification loop. The agent rewrites the hero
based on the screenshot, declares done, and silently ships whatever
the first heredoc produced for the regions it never saw.

5 changes:

- `browser_screenshot` tool gains an opt-in `full_page: boolean`
  parameter (harness-host tool schema).
- API-server tool executor passes the flag through to the desktop
  HTTP service (`desktop-browser-tools.ts`).
- HTTP service implements full-page capture via Chrome DevTools
  Protocol's `Page.captureScreenshot` with `captureBeyondViewport:
  true`, decoding the base64 result back into a NativeImage so the
  rest of the handler is unchanged. Debugger is attached only when
  not already attached and detached after use.
- Polish prompt step 4 explicitly calls
  `browser_screenshot({ full_page: true })` and explains why the
  default viewport-only capture hides the lower regions.
- `APP_BUILDER_TEAMMATE_INSTRUCTIONS` mentions `full_page: true` so
  chat-triggered polishes (which never see the polish prompt) also
  get the guidance.

The default behavior of `browser_screenshot` is unchanged — viewport
remains the default for inspection during interactions; full-page is
opt-in for verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd 2 (holaboss-ai#413)

* refactor(layout): extract theme constants into themes.ts

Move AppTheme/THEMES/THEME_VARIANTS/ThemeVariant/ColorScheme/
ControlCenterCardsPerRow plus their type guards and splitAppTheme()
into a new components/layout/themes.ts. AppShell.tsx, SettingsScreenRoot.tsx
and new-shell/useSettingsState.ts now consume the shared module. Behavior
unchanged for both shells; the toggle is still live.

Side benefit: the new shell's useSettingsState now runs the legacy
combined-key migration too, so users upgrading from a pre-split build
keep their theme/color-scheme preference instead of resetting to
system+holaos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shell): promote NewAppShell and retire the legacy shell

The redesigned shell has been the only one used by anyone running
VITE_NEW_LAYOUT_SHELL=1 or with the Experimental toggle on for a while.
Time to retire the toggle and the legacy AppShell.

- App.tsx always renders the new shell, with a one-shot localStorage
  cleanup for the retired holaboss-new-layout-shell-v1 toggle key.
- SettingsScreenRoot drops the Experimental "New layout shell" row and
  the ConfirmDialog that backed it. The Experimental panel stays for the
  workspace-onboarding mode setting.
- Old apps/desktop/src/components/layout/AppShell.tsx (5.5k lines)
  deleted in full. Its theme types/constants were extracted into
  themes.ts in the previous commit.
- new-shell/ directory renamed to shell/; NewAppShell.tsx renamed to
  AppShell.tsx. Internal symbol NewAppShell renamed to AppShell.
- All external imports (App.tsx, ChatPane, workspaceDesktop,
  AppIntegrationsDialog) updated to the new path.
- Test files updated for the new filenames + symbol name.
- localStorage keys under the new shell keep the "new-shell" prefix on
  purpose — renaming them would silently wipe user preferences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shell): redesign issue detail and add Resume/Reply for blocked board cards

Reworks both the issue board card actions and the detail surface.

Board cards (IssuesBoardPane):
- New blocker-state classifier isBlockedIssueResumable. Sniffs the runtime-
  authored blocker_reason — "Run cancelled by user." or "Run failed[: ...]."
  means the latest subagent run is cancelled/failed and dispatchIssue will
  accept a re-dispatch; anything else (the agent's own waiting_on_user copy)
  means dispatch would 409 and the user must reply with a typed answer.
- Blocked + resumable → "▶ Resume" pill (amber outline) → flips status to
  todo via existing updateIssue IPC; the runtime auto-dispatches a fresh
  subagent on the existing session (runtime-agent-tools dispatchIssue +
  app.ts shouldDispatchIssue branch), so the agent picks up with full
  history.
- Blocked + not resumable → "💬 Reply" pill → opens the detail tab with
  focusComposer=true so the cursor lands in the reply textarea.

Reply focus signal:
- New pendingIssueComposerFocusAtom (Set<tab id>) carries a one-shot
  composer-focus request from the board card to the detail pane.
- useOpenIssueDetailTab gains an optional focusComposer flag that populates
  the set.
- IssueDetailPane runs a useEffect after issue load that focuses the reply
  textarea, scrollIntoView({block:"center"}), and removes its own id from
  the set so re-clicking the tab doesn't re-trigger focus.

Detail pane redesign (Linear-style):
- Drop WorkspaceSurfaceHeader (rounded icon box + 28px title + 5-badge meta
  row was packing too much chrome). Build a compact 44px top bar instead:
  back arrow + workspace breadcrumb + issue id on the left, Edit (or
  Cancel/Save) on the right.
- Main column becomes a clean prose flow: title (28px semibold, inline-
  editable Input in edit mode) → description (whitespace-pre prose, no
  section heading) → blocker banner (only when blocked, left-rule amber
  with inline Resume when resumable) → attachments (chips) → sub-issues
  (StatusDot + mono id + title + assignee row, divide-y line-list — no
  more card-in-card) → activity rule + ConversationTurns → slim composer.
- Right sidebar is now a sticky 240px column of Field rows: Status (with
  pulse and inline Stop/Resume), Priority, Assignee (avatar circle),
  Parent (link), Sub-issues count, Created/Updated/Completed (relative
  time + full-calendar tooltip), Session (mono id).
- Wire the edit affordance that was previously dead code: Edit button
  flips isEditingDetails (disabled while a subagent is running); add
  startEditingDetails/cancelEditingDetails helpers to seed/reset drafts.
- Fix the long-description scroll bug as a side effect — description no
  longer lives inside the now-removed header, so it scrolls with the
  rest of the article.
- Replace SidebarSection/DetailLine helpers with a single Field row
  primitive.
- Drop unused imports (WorkspaceSurfaceHeader, Badge, UserRound,
  CHAT_LAYOUT).

Behavior preserved: all handlers (save, submit reply, stop, attachments,
sub-issue create, streaming), all effects (history load, session stream,
focus composer), runtime IPCs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(shell): tighten board / sidebar / dashboard chrome (round 2 polish)

Sidebar (SidebarIssuesSection):
- Empty workspace-not-selected state with prompt copy instead of dead nav rows.
- "Agent Team" header gets an inline "+" affordance for New issue; drops the
  separate New issue Button-row from the body.
- Compact SidebarNavRow helper for Dashboard / Issues / Teammates — flat
  hover row (h-7) instead of bordered cards, with optional trailing count.
- Active vs Done issues split: Issues nav shows the active count; Done rows
  live behind a separate collapsible section so the list doesn't grow
  unbounded as work completes.
- Status message font bumped from [11px] to text-xs to match neighboring
  copy.

IssuesBoardPane:
- Drop the per-status tinted column chrome (BOARD_COLUMN_CHROME's sky /
  amber / emerald / etc washes) — moves to a neutral palette so the cards
  themselves carry visual weight.
- Expanded icon vocabulary (AlertCircle, CheckCircle2, Circle, CircleDot,
  Eye, Plus, LucideIcon type) — the file is moving toward more
  status-aware affordances; this lands the imports.

WorkspaceDashboardPane:
- Simplify buildDailyBars: signature reduced to (results) → bars, the
  per-call valueForDay / color injection went unused. Each call site now
  builds bars uniformly; color/value treatment moves to render.
- Drop the priority-axis cuts (PRIORITY_ORDER + issuePriorityLabel were
  unused after the dashboard re-skin in this batch).
- Add Square icon + Button import for inline stop-action controls.

⚠️ Sidebar "New issue" handler reverts to setNewIssueOpen(true) — i.e.,
the NewIssueDialog flow rather than the composer-prefill flow PR holaboss-ai#408
landed. This batch was authored before holaboss-ai#408 merged. Decide whether to
keep this revert or re-apply the prefill flow on top.

* ui(chat): tighten trace timeline step + thinking entry

TraceStepGroup:
- Header chip switches to rounded-md (was rounded-lg) and hover token
  moves to bg-fg-4; step-count pill picks up font-medium + bg-fg-6 +
  text-xs + tighter py to read as a real numeric badge instead of
  decorative chrome.
- Group label gets font-medium + text-foreground so it earns its
  hierarchy against muted sub-text.
- Expanded children grow a left border-l indent guide (pl-3.5, ml-1.5)
  so visually-nested entries hang off a real rail rather than just a
  margin.

status.tsx (TraceTimelineStepEntry + ExecutionTimelineThinkingEntry):
- Distinguish hasDetails (>0) vs expandable (>1) so single-detail rows
  no longer pretend to be expandable — collapse the chevron, keep the
  inline detail line on the row itself with min-w-0 truncate.
- Status icon centered in a fixed size-4 grid (was a free-floating
  margin) so all four states (completed / error / live / pending)
  align on the same baseline regardless of icon shape.
- Expanded detail panel: drop the border, switch to bg-fg-2, bump
  rounded-md → rounded-lg + padding, wrap the body in <pre class="font-sans"
  whitespace-pre-wrap> so leading whitespace from runtime-authored
  detail strings survives intact.
- Thinking entry: drop the border + bg-muted shell, switch to a
  borderless tinted pillow so it reads as the agent's quoted inner
  voice rather than a card.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- delete generated zip, blockmap, and *-mac.yml files after the Intel DMG packaging step
- preserve the Intel release contract of shipping only the notarized x64 DMG artifact
- extend the Intel release workflow assertion to cover updater byproduct cleanup
- validation: node --test apps/desktop/electron/intel-macos-release-workflow.test.mjs apps/desktop/electron/release-channel-policy.test.mjs apps/desktop/electron/app-update-config.test.mjs
- build Intel macOS ZIP, blockmap, and updater manifests alongside the notarized DMG
- merge Apple Silicon and Intel macOS updater manifests before publishing shared mac release assets
- add a mac manifest merge helper plus workflow, docs, and test coverage for the shared updater path
- validate with ruby -c apps/desktop/scripts/merge-mac-update-manifests.rb
- validate with ruby -e 'require "yaml"; YAML.load_file(".github/workflows/ci.yml"); YAML.load_file(".github/workflows/publish-macos-intel-desktop.yml")'
- validate with node --test apps/desktop/electron/release-channel-policy.test.mjs apps/desktop/electron/intel-macos-release-workflow.test.mjs apps/desktop/electron/app-update-config.test.mjs apps/desktop/electron/runtime-toolchain-split.test.mjs apps/desktop/electron/mac-update-manifest-merge.test.mjs
- force resolved runtime model budgets to advertise a uniform `maxTokens` value of 128000 across providers and catalog entries
- align the Pi harness fallback max token constant with the shared runtime budget so unknown or unmapped models no longer fall back to 8192
- update provider-config tests for Anthropic, Gemini, and unknown custom models to validate the new 128000 limit
- validation: `npm test -- src/pi.test.ts` in `runtime/harness-host`
Marketing downloads have always wanted "give me the latest stable
build" and "give me the latest beta" as two stable URLs that never
overwrite each other. The flat layout from PR holaboss-ai#410 couldn't deliver
this — a beta release would clobber the stable file name in the
bucket root because the asset filenames are channel-agnostic
(`holaOS-macos-arm64.dmg`, etc.). GitHub Releases dodges this with
its `/latest/download/` route that skips prereleases; R2 has no
equivalent semantics.

Under `desktop/` inside the bucket (lets us share landing-assets
with /videos/ and /images/ without colliding), each release now
uploads every asset to two destinations:

  desktop/<channel>/<name>      — marketing downloads
                                  (latest/holaOS-macos-arm64.dmg,
                                   beta/holaOS-macos-arm64.dmg)
  desktop/tags/<tag>/<name>     — pinned-version downloads

The flat `desktop/<name>` slot is intentionally dropped — anyone
hitting it before this PR was implicitly trusting "whatever release
ran last", which has no defensible meaning across channels.

electron-updater is untouched. It keeps pulling from
`provider: "github"` against holaOS-releases until a future PR
flips electron-builder.config.cjs over to the `generic` provider
pointed at `assets.holaboss.ai/desktop/<channel>/`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… / NewIssueDialog / Favorites + chat ergonomics (holaboss-ai#416)

* feat(sidebar): hover @ button on recent files to mention in chat

Adds a hover-revealed AtSign icon button to the RecentFileRow next to
the existing actions menu. Clicking it inserts an @mention referencing
the file into the current chat composer (uses chatComposerPrefillAtom
with mode="append" — no new plumbing). The same action is exposed as
"Mention in chat" in the row's dropdown for keyboard reach.

Extracts slugifyFilePathForMention() into ChatPane/helpers.ts so the
sidebar and the existing typed-@ flow share one slug rule instead of
two parallel inline regexes that could drift.

The @ button is disabled (with a tooltip explanation) for files outside
the current workspace, where no stable handle can be computed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui(shell): polish TopChrome — tab chip primitive + dead-prop cleanup

Structural:
- Extract TabChip primitive (shared chrome: h-7, hover bg, active tint,
  middle-click close, hover-reveal close button). BrowserTab and
  InternalTab are now thin adapters that only pass in leadingIcon. Cuts
  ~70 lines of duplicated container/close-button code.
- Drop the `driver` prop + per-tab "agent dot" on browser tabs. The
  BrowserStatePayload type has no `driver` field and no caller passed
  it; the modern equivalent is the list-level `controlMode` /
  `controlSessionId` introduced by the operator-surfaces work.
- Replace useAtomValue + useSetAtom for sidebarCollapsed with a single
  useAtom (was reading and writing the same atom via two hooks).

Stale-closure fix:
- close-active-tab IPC subscription was registering once but had a
  closure over handleCloseInternalTab + activeInternalTabId, masked by
  // eslint-disable-next-line react-hooks/exhaustive-deps. Replace with
  ref pattern: subscribe once, ref the latest callback + ids inside.
  useCallback handleCloseInternalTab so the ref reseat is a no-op
  when nothing changes.

Visual:
- Tabs now live in an overflow-x-auto container (with scrollbar
  hidden); the "+ new tab" affordance sits outside it so it can't be
  pushed off-screen. Previously many tabs would silently clip the
  Plus button.
- "+ new tab": icon-button treatment (size-7 grid + bg hover) matching
  the sidebar toggle. Was text-only hover, looked unaffordable next to
  the toggle.
- Hover-reveal close button: 300ms → duration-snappy (180ms) for the
  width/margin reveal, 200ms → duration-fast (120ms) for the opacity
  fade. Tokens, not arbitrary numbers.
- Active tab bg: foreground/[0.07] → foreground/[0.09] for stronger
  selection signal in dark mode without flipping to a hard surface.
- Inactive tab hover also nudges text color foreground/60 → /85 so
  hover reads as "this is hoverable" not just "this region is tinted".
- ScratchGroupChip aria-expanded: drop the border-foreground/25 change,
  keep just text-foreground. The dual change was loud for a "popover
  is open" signal.
- ScratchRow host line: text-foreground/35 → /45 so the secondary URL
  is actually readable.

* ui(shell): polish SearchDialog — surface issues, regroup actions

Substance:
- Add an "Issues" group sourced from useIssues(selectedWorkspaceId).
  Active issues sort above done; capped at 25 rows so the dialog stays
  scannable (anything beyond that wants the board view). Selecting a
  row opens the issue detail tab via the existing
  useOpenIssueDetailTab hook. This closes the biggest "where's my
  work?" gap in ⌘K.
- Split the giant flat "Actions" group into two semantic groups:
    Go to:  Dashboard, Board, Teammates, Inbox, Artifacts, Sessions
    Actions: New tab, Toggle sidebar, Open Control Center,
             Automations, Marketplace, Settings, Create workspace
  "Go to" reads as navigation (open a surface), "Actions" reads as
  imperative (do this). Tighter mental model.
- Add three missing high-frequency shortcuts:
    Toggle sidebar (⌘\)  — pairs with sidebar collapse atom
    Open Control Center (⌘0)  — the CC takeover holaboss-ai#404 shipped
    Surface existing ⌘T / ⌘, shortcuts on their rows
- Fix duplicate icon collisions:
    Automations: LayoutDashboard → CalendarClock (Dashboard kept it)
    Create workspace: Plus → FolderPlus (New tab kept Plus)
    Open Control Center: LayoutGrid (matches CC's workspace grid)
- ActionItem now takes a `LucideIcon` component instead of a rendered
  ReactNode so the chip wrapper renders the icon at consistent size
  without each caller having to set className.
- Update placeholder + empty state copy to match the new scope:
    "Search issues, tabs, workspaces, or actions…"
    "No matches. Try a different keyword." (centered, 6-line padded)

Visual:
- Footer text contrast: foreground/40 → /55 for legibility; separator
  dot /20 → /35 so it's actually visible.
- Tab/Issue/Action selected-state CornerDownLeft uses duration-fast
  (design system token) instead of arbitrary 200ms.
- TabRow host line + agent-tab assignee text bumped from /40 → /55.
- Workspace-active Check icon: /40 → /50 for stronger "this is current".
- Extract RowIconChip primitive for the bg-foreground/[0.06] +
  ring-foreground/5 square that Tab/Action rows share. Issues use a
  bare StatusDot instead — they're work items, not buttons, and the
  chip wrapper would visually compete with the dot color.
- IssueRow shows: status dot + monospaced issue id + truncated title +
  optional assignee name on the right. Mirrors the board's information
  hierarchy at a smaller density.

Imports sorted alphabetical (was: react-hooks injected mid-import).

* ui(shell): polish ChatPanel — align CanvasHeader with TopChrome

CanvasHeader controls:
- "+ New tab" and "Show tabs panel" buttons switch from Button-with-
  text-only-hover to the same raw <button> icon-button treatment
  TopChrome uses (size-7 grid + hover:bg-foreground/[0.05] +
  hover:text-foreground). Header chrome now reads as a single family
  whether you're in split or canvas mode.
- Add ⌘T hint to the New tab title so the shortcut is discoverable
  from canvas mode (only existed in TopChrome and ⌘K before).
- Drop `bg-background/80 backdrop-blur-sm` for a solid `bg-background`.
  The opacity hack on a design token was breaking the project's "no
  opacity hacks on design tokens" rule, and the header sits at the
  top of a clean column — there's nothing under it that benefits
  from frosting.

HiddenTabsDropdown:
- Trigger height 6 → 7 to match the surrounding icon-button rhythm.
- Replace arbitrary `text-[11px]` + `opacity-60` with design tokens
  (`text-xs`, `text-foreground/55` for the chevron).
- Hover bg foreground/[0.04] → /[0.05] to match the rest of the
  header chrome.
- Item hint text-[10px] → text-xs, contrast /40 → /55 so the URL host
  is actually readable.

Resize handle:
- Add `title="Drag to resize · Double-click to reset"` so the reset-
  on-double-click affordance is discoverable. Was undocumented muscle
  memory.

* ui(shell): polish NewIssueDialog — minimum-effort path + token cleanup

Substance:
- Default status to "todo" instead of empty + drop the "status required"
  validation. The minimum path is now: type title → ⌘↵ → done. Was 4
  interactions (type → click status dropdown → pick Todo → click
  Create), now 2 (type → submit). 99% of new issues start in todo and
  users can re-status from the board or detail pane.
- Add ⌘/Ctrl + Enter form-level shortcut. Plain Enter in <Input>
  submits natively (already worked for title); the shortcut covers
  <Textarea> where plain Enter must add a newline.
- "Discard draft" → "Cancel". There's no draft persistence (form
  state resets on close), so "draft" was misleading.
- Move attachment chips out from below the description and right
  under the Attachments cell of the field grid. Chips and the Upload
  button trigger are now adjacent — currently they were separated
  by 200+ pixels of Description + Blocker reason.
- Attachment chip's per-row remove icon: Trash2 → X. "Remove this
  chip from the staging list" is closer to a dismiss action than a
  destructive delete; X reads more accurately and lighter.

Visual:
- All field labels: `text-[11px]` → `text-xs` (6 sites). The arbitrary
  11px was 1px off the design system token without justification.
- Submit Plus icon dropped. The button text "Create issue" already
  states the verb; the leading icon was repeating itself. Spinner
  still appears while submitting.
- Submit button: h-10 min-w-32 → h-9 min-w-28 so it matches the rest
  of the Button rhythm in the dialog (Upload is h-8). Was the
  visually-tallest control with no reason.
- Cancel button text color /45 → /55 to read as "interactive
  secondary action" rather than "decorative footer text".
- Footer gains a ⌘↵ hint chip next to the submit button so the
  shortcut is discoverable instead of muscle-memory-only.
- `bg-destructive/8` → `bg-destructive/10` for the error banner.
  Tailwind opacity steps are 0/5/10/20/…; `/8` is not a valid step
  and was being silently dropped, leaving the banner without a tint.

* test(shell): realign WorkspaceSurfaces fixtures with round 2/3 polish

The shell-promotion + issue-detail redesign + round 2 chrome polish all
landed without this test getting updated. Catches up the assertions to
match the actual shipped surfaces and trims fragile assertions about
specific row classnames / cell counts.

Sidebar:
- New issue is now an aria-labeled icon header button, not a row with
  inline text — assert aria-label instead of literal text.
- Dashboard / Issues / Teammates render through SidebarNavRow; assert
  the prop shape (label="Dashboard" onClick={...}) instead of the old
  bordered card chrome.
- handleNewIssue routes to setNewIssueOpen(true) (the dialog flow);
  the prefill text/sessionMode wiring intentionally retired.
- Empty workspace-not-selected state replaces the silently-disabled
  buttons.
- Done issues split into a collapsible disclosure (showDone state).

SearchDialog:
- Action labels lost the "Open" prefix (Dashboard / Board / Teammates).

IssuesBoardPane:
- BOARD_COLUMN_CHROME (tinted per-status column shells) is gone — drop
  that assertion; status filter + ordering remain load-bearing.
- "Add issue to ${status}" aria label format change.

IssueDetailPane:
- "Related issues" → "Sub-issues" header.
- Parent issue moved into a Field row, not inline "Sub-issue of …"
  copy.
- WorkspaceSurfaceHeader presence assertion dropped — header is now
  managed locally per pane.

TeammatesPane:
- Search placeholder updated.
- Detail tabs come from a typed tuple — assert the values, not the
  JSX trigger nodes.

WorkspaceDashboardPane:
- Card titles change every iteration; trim those assertions and trust
  the data-shape / IPC invariants below.

* feat(shell): sidebar Favorites — pin issues, files, URLs

Recents (auto, time-decayed) doesn't cover "the things I actually
return to every day" — the PRD doc, the standup issue, the Notion
page. High-frequency items eventually get cycled out by one-off
work, and there's no way to promote anything. Adds a user-curated
Favorites section that survives recents churn and reloads.

State (state/favorites.ts):
- New discriminated-union FavoriteItem with three kinds:
    issue — per-workspace, sidebar filters to current workspace
    file  — per-workspace (or null for non-workspace files); same
    url   — global, visible across all workspaces
- favoritesAtom is a single flat atomWithStorage array under
  "holaboss-shell-favorites-v1". Workspace filtering happens at
  render time so the storage shape stays stable across switches.
- toggleFavoriteAtom: add-if-absent / remove-if-present, keyed by a
  stable composite key (`issue:${ws}:${id}` etc.) so starring the
  same thing twice from different surfaces never duplicates. New
  entries land at head (most-recent first).
- favoritesForWorkspaceAtom returns (workspaceId) => filtered list:
  always all starred URLs + matching workspace's issues/files.
- isFavoriteAtom returns a reactive predicate so star buttons can
  read state without re-subscribing to the full list.
- MAX_ENTRIES = 200 — generous; users who hit this are power-users
  who can prune via the unstar action.

Sidebar UI:
- FavoritesSection renders above Recents in SidebarHomeSection.
  Hidden when empty so the section doesn't add visual weight until
  the user starts pinning.
- FavoriteRow handles all three kinds: kind-appropriate leading icon
  (CircleDot / FileTypeIcon / favicon-or-Globe), click opens via
  the matching pathway (issue detail tab / internal file tab /
  browser tab), hover-reveal X removes.
- IssueListRow gains an absolutely-positioned hover-star in the
  top-right. Starred state shows fill-current foreground/70;
  unstarred only on hover. Star sits next to ChevronRight to avoid
  the nested-button anti-pattern (sibling of the card button, not a
  child).
- RecentFileRow gains a hover-reveal star alongside Mention and the
  more-actions dropdown. Width of the action area expands from
  w-11 → w-[68px] to fit the third button. Always-visible when
  already starred so the affordance stays discoverable.
- RecentRow (URL) gains the same pattern: hover-star + dropdown
  trigger in the action area (w-5 → w-11).
- All three star buttons share the visual convention: hover/starred
  uses foreground/70 with bg-foreground/[0.06] on hover, idle uses
  foreground/50. Star icon is fill-current when starred (clear
  on/off signal), outline when not.

Follow-up:
- BrowserTab in TopChrome doesn't get a star this round — TabChip's
  hover-close area is already tight at 14px; adding a second action
  would need width-shaping work. URL favorites are still reachable
  via the sidebar Recent row, which is the more discoverable
  starting point anyway.

* test(shell): catch up assertions to round-3 shell wiring

A handful of source assertions had drifted past the round-2/3 polish:

- AppShell now has three top-level branches (onboarding takeover,
  control-center takeover, default chrome). The old single big regex
  doesn't match; split into one anchor-element check per branch.
- SidebarIssuesSection routes through SidebarNavRow and opens the
  NewIssueDialog atom — the composer-prefill "New issue: " path is
  gone. Updated IssuesSidebar / NewIssueDialog / ChatPanel.prefill to
  assert the new wiring (setNewIssueOpen, aria-label, navrow labels).
- Mac stoplight detection moved to useStoplightCompensation(); the
  workspacePopoverAlignOffset variable name changed accordingly.

No app code is changed in this commit; the tests now describe the
shell as it actually is rather than how it used to be.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(shell): retire legacy combined-theme storage key

useSettingsState no longer writes "holaboss-theme-v1" — its only
historical consumer was the now-deleted AppShell. App.tsx generalises
the previous one-shot localStorage cleanup into a small list of
retired keys and removes both the legacy shell-toggle key and the
combined-theme key on boot. The legacy migration in loadColorScheme /
loadThemeVariant still backfills the split form for users on a very
old install, so first boot after upgrade keeps their preferences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(shell): Recents star is hover-only, not always-visible-when-starred

In RecentRow and RecentFileRow the star button stayed visible after
starring — clutter, because starred URLs/files already appear in the
Favorites section directly above. The always-on signal was redundant
visual weight in the Recents list.

Make the star purely hover-revealed in both rows. The "is this
starred?" answer in Recents is conveyed by the item's parallel
presence in Favorites, not by an inline icon.

IssueListRow keeps its always-visible-when-starred behavior — it lives
in the issues board section where there's no parallel "Favorites
issues" group above to carry that signal.

* chore(shell): drop two orphans surfaced by the shell promotion

- WorkspaceSurfaceHeader.tsx: no consumers anywhere across desktop/
  src or electron/. Each pane manages its own header now; this file
  was a leftover from an earlier pattern where every workspace
  surface shared one chrome component.
- useEscapeToClose: zero importers. Radix dialogs / Popovers handle
  ESC natively and Sidebar shortcuts are routed through their own
  keyboard layer; nothing was calling this hook anymore.

Also strips the corresponding fixture + assertions from
WorkspaceSurfaces.test.mjs. Nine shell + auth tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(shell): drop WorkspaceSurfaceHeader fixture references

The file was deleted in 76e15392; this is the test-side cleanup that
went out separately. Fixture array, Promise.all entry, and the three
assertion lines that read surfaceHeaderSource are all gone now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat): persist composer drafts per workspace

ChatPane already accepted composerDraftText + onComposerDraftTextChange
props with a debounced hydrate/publish loop, but no caller wired them —
so drafts evaporated on unmount. Typing a paragraph, switching to the
issue board, then switching back lost everything.

Wire the props through ChatPanel against a new atom.

state/composerDrafts.ts:
- composerDraftsAtom: atomWithStorage<Record<workspaceId, string>>
  under "holaboss-shell-composer-drafts-v1". Persists across reload.
- Keyed by workspaceId only. Sub-session granularity would carry a
  refactor cost without daily-flow payoff; the dominant case is "got
  distracted, came back to the same workspace's chat".
- Read accessor (composerDraftForWorkspaceAtom) returns a selector
  function so callers can pick their workspace's draft without
  resubscribing to the full record.
- Write atom (setComposerDraftAtom) takes {workspaceId, text} and
  prunes empty entries — a submit clears the draft via the existing
  setInput("") in ChatPane, which flushes through to "" here and
  drops the key entirely.

ChatPanel.tsx:
- Read draft for current workspace + pass to ChatPane.
- Pass setter that writes to the atom keyed by current workspaceId.

Submit-clear, prefill-overwrite, prefill-append, workspace-switch
hydration are all handled by ChatPane's existing draftHydration
effect — no changes needed there.

* ui(chat): rework the away-from-bottom affordance into a labeled pill

There was already an `isAwayFromChatBottom` button — a tiny size-8
ChevronDown circle pinned to bottom-center. Three problems:
- Centered position conflicts with the composer center column visually
  ("is this part of the chat or the input?").
- Same chrome whether the user just scrolled up to read history OR
  whether the agent is actively streaming new text — the second case
  needs a much louder signal, the first is fine being quiet.
- No label. An unmarked ChevronDown bubble doesn't read as "the
  conversation is still moving below you".

Switch to a right-edge pill at bottom-3 right-3 with mode-dependent
content:
- isResponding → pulsing primary StatusDot + "Resume tail". The dot
  reads as "something live is happening below"; the verb invites
  rejoining without sounding like a destructive jump.
- not responding → ChevronDown + "Latest". Plain "scroll-to-bottom".

Right-edge anchor stays out of the composer column's mental space and
doesn't collide with the centered max-w content well.

* fix(chat): Escape on mention/slash picker actually dismisses

The picker dismissal was tracked by a composite key
"start:end:query" — every keystroke advances range.end, so the dismissed
key would no longer match activeKey on the very next character. The
clearing effect then nulled the dismissal, and the picker silently
re-opened. Net effect: Escape was a one-frame no-op for both pickers.

Track the dismissed range's `.start` instead. That stays constant
across keystrokes while the caret is still inside the same logical
mention (typing more chars only advances .end). The picker stays
hidden until the caret actually leaves that range — either by
backspacing past the anchor (range becomes null) or by a fresh
mention/slash starting somewhere else (.start differs).

Also fixes a related quirk on slash Escape: it was setting
caretIndex to -1 as an indirect way to invalidate the active range
(which made findActiveSlashCommandRange return null on the next
render). Replace with the same dismissed-anchor mechanism so the
slash and mention paths behave the same way.

The post-apply dismissal (after picking a mention from the menu)
keeps the same intent but now anchors against the pre-insertion
start, which the effect clears once the caret has crossed the
post-insertion range boundary.

* ui(chat): replace text-[10px]/[11px] in message renderers with text-xs

The project's text-xs is 0.725rem (~11.6px) — already smaller than
Tailwind's default and a real design-system token. Sites going below
it via arbitrary text-[10px] are just going under-scaled with no
justification, and text-[11px] sits 0.6px off the token for no reason.

Files touched (all in the message stream):
- UserTurn timeLabel
- AssistantTurn timeLabel
- ChatHeader subtitle
- BackgroundTaskReferenceCards status pill
- IntegrationProposalCard "Reconnect" hover hint + error banner

Visually the bump from text-[10px] → text-xs is ~1.6px on meta labels;
text-[11px] → text-xs is ~0.6px. Neither changes the layout but the
copy becomes legible in dark mode at typical viewing distance — the
former meta labels were squinting territory.

Skipped:
- `bg-fg-6 dark:bg-card` on AssistantTurn. Both sides are real tokens.
  The pattern intentionally bumps dark-mode contrast (per-theme --card
  overrides land 4-6% above --fg-6 in dark), so the dual-token is
  semantic, not an opacity hack.
- text-[11px] in IssueThreadControls + QueuedSessionInputRail. Those
  are composer-adjacent surfaces; defer to a separate composer pass
  if needed.
- text-[13px] in QueuedSessionInputRail. Falls between --text-xs and
  --text-sm; tightening that one wants a coordinated bump rather
  than a per-site swap.

* ui(chat): narrow user bubble max-width 80% → 75%

80% was generous — at canvas-mode max-width (760px content well) the
bubble could reach ~608px wide, which is a lot of horizontal travel
for a reading eye when the bubble is right-aligned. Linear / ChatGPT
/ Cursor all sit in the 70-75% range for the same reason.

75% trims ~5% off without truncating short messages (they're already
narrower than the max). Long-form messages get the same generous
internal wrap, just anchored a little more right.

AttachmentList renders as a sibling outside the bubble (already), so
the bubble narrowing doesn't constrain attachment layout — they
continue to use the same 75% max via the wrapping flex container.

* fix(chat): respect user trace-group toggle during streaming

The group already auto-collapses when assistant output starts streaming
(line 92-96) so the trace gets out of the way of the answer — good
default. But: if the user explicitly clicked the chevron during the
trace phase to keep it expanded, the same effect would slam it shut
the moment output began. Their explicit "I want to follow this" got
overridden by the system's "answer is more important".

Track a userOverrodeRef flag on the toggle button. While that flag is
set, both auto-snap branches in the live/liveOutputStarted effect
become no-ops (we still update the previous-state refs so future
transitions are detected correctly if the override is later cleared).

A `forceExpand` action from the assistant footer menu still wins —
that's a fresh user-initiated system signal ("show me the details
again"), so it resets the override at the same time it expands.

State scope: the flag is per-component instance, so a fresh assistant
turn / remount starts clean. Auto-collapse on the next turn isn't
affected by a previous turn's override.

* ui(chat): ChatHeader gains a stream-pause + Inbox dot loses destructive

Two fixes in the same header — both about correct semantics, not
chrome.

1. Stream pause was composer-only. On a long run the user scrolls up
to follow trace / read earlier output, the composer drops off-screen,
and the only way to halt the agent is to scroll back down to find
the pause button at the input. Add a header-side Pause button
mirroring the composer's pause:

- Only visible while isResponding && onPause is wired.
- Disabled while pausePending / pauseUnavailable, matching the
  composer's gating.
- Shows Loader2 spin during pausePending, Square (filled) otherwise.
- Tooltip swaps to "Pausing…" during pending state.
- Wired from ChatPane via existing pauseCurrentRun + isPausePending
  + isSubmittingMessage state — no new IPC, no new lifecycle.

2. Inbox unread dot was using StatusDot variant="destructive". That
variant is for error states (matches its name); new mail isn't an
error. Switch to variant="primary" so the dot reads as a
positive/neutral notification anchor.

The destructive color was also visually flagging "something wrong
here" each time a workspace had pending inbox items, which is the
wrong emotion for "you have things to look at". Primary maintains
visual prominence (it's the brand color) without overloading
"something broke".

* ui(artifacts): smarter title fallback chain for outputs without titles

Many output records arrive with title === "" — module-app creates
(POST /api/v1/outputs that doesn't pass a title) and forwarded
sub-agent deliverables where the runtime skipped the basename fallback
were the two dominant cases. The UI then showed either "Untitled
artifact" (ArtifactsPane + modal) or "${kind} #${n}" (AssistantTurn
inline), making rows visually indistinguishable.

Add outputDisplayTitle(output, fallback?) in ArtifactBrowserModal.tsx
as the single source of truth. Priority order:

1. output.title — wins when set (the dominant case stays unchanged)
2. metadata.summary — written by report-style tools (write_report
   sets this in claimed-input-executor.ts). Trimmed to a single line,
   truncated to 64 chars with an ellipsis.
3. file path basename — preserves the agent's filename even when
   title didn't propagate upstream
4. capitalized metadata.artifact_type — for app outputs the
   "Tweet" / "Post" reads better than "${kind} #${n}"
5. caller fallback → "Untitled artifact"

Threaded through three call sites:
- AssistantTurnOutputs ArtifactRow.displayTitle (replaces the
  ad-hoc `output.title?.trim() || defaultTitle || "Untitled artifact"`
  chain). The per-kind counter (defaultTitle) still wins last so
  inline rows in a single reply stay disambiguated.
- ArtifactBrowserModal artifact row label.
- ArtifactsPane row label AND the search-query matcher — so an
  output whose display label came from a metadata fallback is now
  searchable against that visible text rather than only the raw
  (empty) title.

Status field is intentionally still ignored — it's set to "completed"
across runtime call sites and the desktop never differentiated by
status. Nothing in this commit relies on draft/scheduled/published
semantics that this product doesn't have.

* ui(artifacts): group ArtifactsPane rows by recency

A flat latest-first list works for 20 outputs; for 200 it just scrolls
forever and the user can't tell where "yesterday's work" stops and
"three weeks ago" starts.

Add groupOutputsByTime — buckets the (already-filtered, already-sorted)
list into Today / Yesterday / Earlier this week / Earlier, then renders
each with a small uppercase section header (text-xs tracking-wide /70).
Empty buckets drop out so the headers never dangle.

Bucket boundaries are local midnight + a rolling 7-day "this week"
window. Anything older falls into Earlier — month/quarter buckets
would pretend at a precision the workspace's actual output volume
usually doesn't earn for personal artifacts.

Behavior preserved: filter, search, latest-first sort within each
group, click → onOpenOutput. Search results stay grouped (rather than
flattening) so the user keeps temporal context while filtering.

Drive-by: changeLabel font size text-[10px] → text-xs (the project's
xs token is 0.725rem / ~11.6px, still subtle but no longer below the
smallest design token). The visual promotion of Created/Updated as
its own affordance comes in a follow-up commit.

* ui(artifacts): promote Created/Updated change indicator with a status dot

The change_type signal was rendered as an uppercased "CREATED" /
"UPDATED" text strip at /70 contrast on the right edge of every
artifact row. Functionally legal but visually it scanned as decorative
chrome — users couldn't quickly tell "agent just made this" from
"agent modified an existing one" without reading.

Extract OutputChangeBadge — a tiny StatusDot + label combo, shared
between ArtifactBrowserModal and ArtifactsPane.

- created → success dot + "New"  (green)
- modified → info dot + "Updated" (blue)
- anything else → null (callers don't need a guard)

Copy switches "Created" → "New" to keep parity with "Updated" length
and to lean into the dot's visual freshness signal. Label text stays
muted-foreground so the dot carries the only color in the row,
leaving the title to dominate.

Modal version drops the bordered Badge primitive in favor of the same
shared component, and the "kindLabel" eyebrow above the title bumps
text-[10px] → text-xs while we're in there (still smaller than the
title at /muted-foreground but no longer below the smallest design
token).

* feat(artifacts): star outputs from ArtifactsPane → Sidebar Favorites

Extends the Favorites system to cover workspace outputs alongside the
existing issue / file / url kinds, so the artifact a user keeps
returning to (the PRD report, the weekly metrics doc, the agent-built
post they're tracking) can be pinned in the sidebar.

state/favorites.ts:
- New "output" variant on FavoriteItem: { workspaceId, outputId, title }.
  Intentionally minimal — storing the full WorkspaceOutputRecordPayload
  would go stale (module routes / file paths can change), so we keep
  just the pointer and re-resolve at open time.
- favoriteKey gains an output case: `output:${ws}:${outputId}`.
- toggleFavoriteAtom accepts the new shape.
- favoritesForWorkspaceAtom comment clarified — all of issue / file /
  output filter by workspaceId; URLs stay global.

ArtifactsPane:
- Extract ArtifactRow component (hover scaffolding now justifies a
  primitive). Adds an absolutely-positioned hover-star at the right
  edge, matching the visual convention from issue / recent file rows
  (text-foreground/70 when starred, /50 on hover, fill-current Star).
- Stays hover-only — the parallel presence in the FavoritesSection is
  the "this is starred" signal once the row exits.

Sidebar FavoriteRow:
- Output case opens via listOutputs(workspaceId) → find by id → call
  the existing openOutput from useOpenWorkspaceOutput. One extra IPC
  per click; the workspace's output list is bounded (limit 500). If
  the output was deleted between star and click, silently no-op so
  the user keeps the option to prune the entry.
- Icon: Package (the same icon Sidebar uses for Artifacts everywhere
  else) so the row reads as "this is an artifact" instead of looking
  like a misplaced file row.
- Label / title attr fall back to the title snapshot, then outputId,
  then "Untitled artifact".

Out of scope (deferred):
- Hover-star on inline AssistantTurnOutputs rows. The chat surface is
  already hover-busy with copy / open affordances; the ArtifactsPane
  is the discoverable entry point and starring there reaches the
  same global Favorites section.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey Li <jeffreyli@imerch.ai>
…olaboss-ai#414)

* feat: upgrade pi read and edit tools to hashline patches

- add repo-local hashline read/edit tool overrides for Pi harness sessions instead of patching the upstream pi-coding-agent package directly
- change read to emit snapshot-tagged numbered output so follow-up edits can anchor to the exact file view the model saw
- change edit to accept single-string hashline patch input, preflight multi-file batches, reject stale tags, and return the next snapshot header after writes
- extend workspace boundary enforcement to inspect hashline section headers inside edit.input so the new payload format cannot bypass local path restrictions
- add focused harness-host and workspace-boundary regression tests covering snapshot reads, anchored edits, stale-tag rejection, and multi-file preflight behavior
- validation: npm run typecheck
- validation: node --import ./node_modules/tsx/dist/loader.mjs --test src/pi-hashline-tools.test.ts
- validation: node --import ./node_modules/tsx/dist/loader.mjs --test ../harnesses/src/workspace-boundary.test.ts
- validation: npm test

* fix(runtime): propagate operator timezone through cronjobs and prompts

- persist runtime user profile timezone through control-plane storage, Electron auth fallback, and renderer typings
- include operator timezone in agent prompt context and ts-runner current-user hydration so relative dates resolve against the user profile
- pin cronjob metadata to a resolved timezone and compute next-run scheduling from the runtime user timezone instead of the host default
- update regression coverage for runtime profile persistence, cronjob scheduling, ts-runner hydration, agent prompt/config context, and raw cronjob routes
- validation:
  - `bun --filter=@holaboss/runtime-state-store run typecheck`
  - `bun --filter=@holaboss/runtime-api-server run typecheck`
  - `bun --filter=holaboss-local run typecheck`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runtime user profile round trip preserves manual value and auth fallback only fills when empty" src/store.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="cronjob helpers honor next_run_at and preserve legacy scheduling fallback" src/cron-worker.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="composeBaseAgentPrompt includes current user context when provided" src/agent-runtime-prompt.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="projectAgentRuntimeConfig includes current user context as a context message" src/agent-runtime-config.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runTsRunnerCli loads current user context from the runtime profile" src/ts-runner.test.ts`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runtime tools capability routes expose local onboarding and cronjob actions|cronjobs and session state routes preserve local payload shape" src/app.test.ts`
  - `node --test ../apps/desktop/electron/control-plane-owned-state.test.mjs`

* feat: streamline teammate creation and dashboard cards

- replace direct teammate creation in the teammates pane with a lightweight dialog that collects name and role
- route teammate creation requests through the ensured main session so the coordinator can follow the create-teammate skill and ask follow-up questions in chat
- remove duplicated success-rate cards from the workspace dashboard and add a waiting-for-review summary card
- update workspace surface tests to cover the new teammate creation flow and revised dashboard copy
- validation: bun test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs
- validation: npm --prefix apps/desktop run typecheck

* feat: add built-in HR teammate and remove preferred tool routing

- remove teammate preferred tools from the desktop UI, API payloads, runtime capability schemas, routing records, and persisted capability-profile model
- seed a built-in HR system teammate for every workspace alongside General and keep both system teammates normalized on read/write paths
- update teammate creation prompts and the embedded create-teammate skill to treat teammate requests as production bootstrap work with integration checks and teammate-local skills when needed
- adjust teammate routing and tests to rely on capabilities, summaries, instructions, and skills instead of preferred-tool matching
- refresh desktop and runtime tests for the HR teammate roster and the simplified capability profile shape
- validation: bun test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs
- validation: npm --prefix apps/desktop run typecheck
- validation: bun run typecheck (runtime/state-store)
- validation: bun run typecheck (runtime/api-server)
- validation: bun test runtime/api-server/src/teammate-routing.test.ts
- validation: bun test runtime/api-server/src/teammate-skill-files.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-config.test.ts
- note: DB-backed runtime test suites remain blocked in this shell by the existing better-sqlite3 native module/runtime mismatch

* fix: route hashline edit activity through task followups

- make hashline edit parsing tolerate unified-diff style body rows and retro-strip context prefixes within a hunk
- parse hashline `¶path#TAG` edit headers in the desktop chat trace so edit calls resync the edited file display and show file-focused activity details
- record edited file paths in runtime tool usage summaries and carry them into subagent/main-session follow-up payloads so edit-only tasks materialize concrete user-facing updates
- add regression coverage for hashline parsing, chat-pane file sync, turn summaries, and main-session event prompt payloads
- validation: `node --import ./node_modules/tsx/dist/loader.mjs --test src/pi-hashline-tools.test.ts`, `node --test --test-name-pattern="chat pane syncs the shared file display from live file-oriented tool calls" apps/desktop/src/components/panes/ChatPane.test.mjs`, `node --import tsx --test --test-name-pattern="compactTurnSummary surfaces edited files when a run completed without assistant text|queued main-session event prompt entry preserves edited file paths for follow-up routing" src/turn-result-summary.test.ts src/main-session-event-prompt.test.ts`, `npm run typecheck`

* feat: tighten teammate delegation and streamline onboarding surfaces

- require explicit teammate ids for delegate_task across tool schemas, client normalization, prompt guidance, and runtime validation so managers choose assignees intentionally
- render teammate roster entries with canonical ids to prevent name-vs-id delegation failures like the U4 General/general mismatch
- shift onboarding flows and tests away from lab-backed wording toward source-controller onboarding and updated workspace language
- simplify the issue detail surface by removing mutable properties controls from the sidebar and aligning related desktop test expectations
- add the stop-slop embedded skill bundle for stricter teammate output quality guidance
- validation: bun test runtime/harness-host/src/pi-runtime-tools.test.ts
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts
- validation: bun test runtime/harnesses/src/capability-http.test.ts --test-name-pattern "runtime delegate-task client forwards use_user_browser_surface when explicitly requested"
- validation: bun test runtime/harness-host/src/pi-runtime-tools.test.ts --test-name-pattern "Pi runtime subagent tools normalize delegated task bodies and control routes"
- validation: bun test runtime/api-server/src/agent-runtime-prompt.test.ts --test-name-pattern "teammate routing context"
- note: bun test runtime/api-server/src/runtime-agent-tools.test.ts remains blocked here because Bun does not support better-sqlite3 in this environment
- note: bun run typecheck in runtime/api-server remains blocked by existing unrelated ONBOARDING_ALIGNMENT_STATE errors in src/app.test.ts

* fix(main-session): route issue creation through composer

- route the new-shell `New issue` sidebar action through the chat composer prefill flow with `New issue: ` instead of opening the modal directly
- remove `teammates_create` and `teammate_skills_create` from the main-session runtime tool allowlist while leaving delegated/subagent behavior unchanged
- update new-shell sidebar source tests and the focused ts-runner allowlist assertion to match the revised main-session surface
- validation:
  - `node --test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs`
  - `node --test apps/desktop/src/components/layout/new-shell/NewIssueDialog.test.mjs`
  - `node --test apps/desktop/src/components/layout/new-shell/IssuesSidebar.test.mjs`
  - `node --import tsx --test --test-force-exit --test-name-pattern="runTsRunnerCli keeps main workspace sessions on a coordinator surface" src/ts-runner.test.ts`
  - `bun --filter=@holaboss/runtime-api-server run typecheck`

* feat: refine direct workspace onboarding flow

- expand the workspace onboarding alignment report contract to cover user intent, work context, research basis, integrations, teammates, workspace rules, apps, teammate-owned cronjobs, and implementation notes
- update onboarding report sanitization, review UI labels, and onboarding prompt expectations to match the v2 report shape
- keep workspace_onboarding implementation in the same session by excluding delegated task tools from the onboarding capability surface and rejecting delegation at runtime
- update onboarding runtime tests for the v2 report payload and for zero background tasks in direct workspace onboarding
- Validation: bun --filter=@holaboss/runtime-api-server run typecheck
- Validation: node --import tsx --test --test-force-exit src/agent-capability-registry.test.ts src/agent-runtime-prompt.test.ts

* feat: restrict teammate bootstrap to HR sessions

- gate the embedded create-teammate skill and teammate bootstrap runtime tools so only HR-owned sessions can see or invoke them
- route front-session teammate bootstrap requests through HR, tighten prompt guidance, and enforce the restriction in the API/runtime service layer
- add delegated task continuation via reply_task, return stable task_id payloads to managers, and stop exposing top-level subagent_id in delegate_task responses
- preserve or draft chat prefills intentionally for issue and automation flows, and make embedded-skill markdown changes invalidate the staged desktop runtime bundle
- extend desktop and runtime tests for skill visibility, runtime projection, task follow-up routing, and chat prefill behavior
- validation: node --test apps/desktop/src/components/layout/new-shell/ChatPanel.prefill.test.mjs
- validation: node --test apps/desktop/scripts/runtime-bundle-state.test.mjs
- validation: node --import tsx --test src/workspace-skills.test.ts
- validation: node --import tsx --test --test-name-pattern "composeBaseAgentPrompt includes teammate routing context when provided" src/agent-runtime-prompt.test.ts
- validation: node --import tsx --test --test-name-pattern "runTsRunnerCli exposes teammate bootstrap runtime tools only to the HR subagent|runTsRunnerCli includes teammate-local skills for assigned subagent runs" src/ts-runner.test.ts

* feat: refine onboarding orchestration and specialist routing

- evolve workspace onboarding toward the new alignment-report and implementation-orchestration flow, including direct source-workspace onboarding sessions and phase-aware execution rules
- thread onboarding state through runtime config, capability manifests, and ts-runner projection so alignment hides implementation-only tools while implementation can delegate work
- add teammate roster listing plus HR/App Builder session guards for teammate provisioning and managed app runtime tools
- introduce the App Builder system teammate and update routing, workspace skills, and embedded tool guidance so app work routes away from General
- pass session ids through runtime app capability routes and expand state-store system teammate ordering/backfill behavior for the specialist roster
- update prompt, runner, runtime-tool, state-store, and routing tests to cover onboarding phases, specialist capability surfaces, and delegated execution behavior
- Validation:
  - bun --filter=@holaboss/runtime-api-server run typecheck
  - node --import tsx --test --test-name-pattern="workspace-onboarding" src/agent-capability-registry.test.ts src/ts-runner.test.ts

* fix: restore HR teammate skill loading in packaged runtime

- resolve HR delegated-run skill scoping from the assigned issue or subagent record before falling back to session context
- stop the PI harness from auto-injecting every embedded skill and honor only explicitly projected skill directories plus workspace-local skills
- allow workspace-boundary reads inside approved external skill directories so bundled runtime skills can be loaded without opening arbitrary paths
- fix the embedded create-teammate skill frontmatter so the real HR skill is discoverable by the workspace skill loader
- simplify the manual teammate-creation prompt to route directly through the built-in HR teammate
- add regressions for stale teammate context, real embedded HR skill discovery, PI skill-dir projection, and workspace-boundary external skill access
- restage the local macOS runtime bundle so the desktop app can pick up the fixed embedded skill
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0
-
- bun test v1.3.11 (af24e281)
- ✔ workspace surfaces wire board and dashboard tabs through the shell (5.117125ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 45.816458

* fix: align runtime api tests with merged cronjob behavior

- update cronjob route and workspace-lab expectations to include the pinned timezone metadata now attached by the runtime
- pin the cron worker next_run_at test to UTC so nextRunAt remains authoritative for the due-check path under the current scheduler logic
- update delegated task assertions to read the latest subagent id from the stored child-session run instead of assuming a latest_run payload on delegateTask manager responses
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0

* test: align runtime api server expectations with merged capability context

- include the now-present  field in capability-manifest context assertions for subagent and main-session tests
- update delegated task route fixtures to provide the required  on capability-route requests
- assert the parsed skill route  payload instead of matching against JSON-escaped response text
- Validation:
- @holaboss/runtime-api-server typecheck: Exited with code 0
-

* feat: add optional sub-issues across runtime and desktop

- add optional parent-child issue relationships to the state store, runtime API, and delegated task payloads
- add desktop related-issues UI and inline sub-issue creation while keeping top-level issues valid
- expose parent issue metadata through Electron bridge and runtime capability clients
- fix workspace runtime DB bootstrap so older issue tables migrate parent_issue_id before parent-based indexes are created
- update store and desktop surface coverage for the new issue hierarchy behavior
- validation: npm --prefix runtime/state-store run typecheck
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix apps/desktop run typecheck
- validation: node --test apps/desktop/src/components/layout/new-shell/WorkspaceSurfaces.test.mjs apps/desktop/src/components/layout/new-shell/NewIssueDialog.test.mjs
- validation: node apps/desktop/scripts/ensure-runtime-bundle.mjs

* fix: hydrate delegated task payloads in runtime API responses

- hydrate newly delegated tasks through getTask before returning from the runtime capability subagents route
- keep the runtime-agent-tools service contract unchanged while restoring the manager-facing HTTP payload shape with latest_run details
- fixes runtime-api-server CI assertions that expect delegated task responses to expose the full task record instead of a stripped run payload
- validation: npm --prefix runtime/api-server run typecheck

* fix: preserve delegated task issue_id alias

- restore issue_id on manager-facing delegated task payloads alongside task_id
- keep hydrated task responses backward compatible with runtime-api-server delegated task tests and existing consumers
- validation: npm --prefix runtime/api-server run typecheck

* fix: align delegated task route test with hydrated task payload

- update the runtime API delegated task test to assert issue fields at the top level and run metadata under latest_run
- keep child session lookups aligned with the hydrated task payload returned by the subagent creation endpoint
- validation: bun --filter=@holaboss/runtime-api-server run typecheck

* feat: expand hashline reads and promote ripgrep tooling

- expand Pi hashline read support to cover directory pagination, PDF extraction, DOCX extraction, and clearer binary-file handling\n- add regression coverage for the richer hashline read surfaces in the harness-host test suite\n- promote ripgrep to the primary projected search tool while keeping the legacy grep request key as a compatibility alias\n- update runtime tool manifests, default tool sets, and workspace boundary policy to recognize ripgrep as a first-class local inspect tool\n- keep glob exposure unchanged and retarget the PI host/tool-projection path so runtime sessions now see ripgrep instead of grep\n- Validation: bun test runtime/harness-host/src/pi-hashline-tools.test.ts; bun test runtime/harness-host/src/pi.test.ts --test-name-pattern "filterPiToolDefinitionsForRequest"; bun test runtime/api-server/src/ts-runner.test.ts --test-name-pattern "runTsRunnerCli keeps main workspace sessions on a coordinator surface|runTsRunnerCli stages browser tools for subagent executor sessions and strips orchestration runtime tools"

* feat: reframe onboarding prompt around forward deployment

- update the workspace onboarding controller prompt to frame the agent as a user-facing forward deployment engineer instead of an architect
- shift alignment guidance toward understanding the user's business context, optimizing for time-to-working-solution, and choosing a thin high-leverage first slice
- reinforce that alignment should produce an implementation-ready plan instead of a generic brainstorm
- update prompt assertions to cover the new forward-deployment-engineer language and implementation bias
- Validation:
  - bun test runtime/api-server/src/agent-runtime-prompt.test.ts
  - bun --filter=@holaboss/runtime-api-server run typecheck

* feat: align base file tools with oh-my-pi workflows

- replace legacy grep/glob exposure with first-class search/find tools across the harness host, runtime tool projection, and docs
- upgrade read with structural summaries, line selectors, hashline-aware write interoperability, shared snapshot storage, and zip member reads
- add grouped search with context frames, archive-member search, shared hashline tags, and sparse snapshot recording for emitted match windows
- add oh-my-pi-style find behavior with multi-path support, grouped output, timeout partials, and tolerant missing-path handling
- update tests and runtime surface expectations to cover the new host-native file navigation and hashline behavior
- validation:
  - npm --prefix runtime/harness-host run typecheck
  - npm --prefix runtime/api-server run typecheck
  - npm --prefix runtime/harness-host test -- --test-name-pattern "filterPiToolDefinitionsForRequest|find groups results by directory and orders them by recent mtime|find tolerates missing paths in a multi-path call|find returns an exact file path directly|find keeps legacy pattern/path compatibility while preferring paths|search groups matches by directory and file|search respects file line selectors|search includes surrounding context lines and elides gaps|search paginates by file window with skip|archive|hashline read|hashline write|base edit remains"
  - node --import tsx --test --test-name-pattern "runTsRunnerCli keeps main workspace sessions on a coordinator surface" src/ts-runner.test.ts
  - node --import tsx --test --test-name-pattern "runTsRunnerCli stages browser tools for subagent executor sessions and strips runtime browser duplicates" src/ts-runner.test.ts

* feat: clarify onboarding alignment stopping rules

- add an explicit alignment stopping rule so the onboarding FDE digs only until the plan is implementation-ready
- instruct the onboarding prompt to follow the user's signal when they say the plan is ready unless a real blocker or safety-critical ambiguity remains
- mirror the new stopping-rule and user-readiness guidance in the workspace onboarding prompt test assertions
- Validation:
  - bun test runtime/api-server/src/agent-runtime-prompt.test.ts
  - bun --filter=@holaboss/runtime-api-server run typecheck

* feat: align harness base tools and harden runtime entrypoints

- extend the harness host read/search/find/edit workflow toward oh-my-pi parity, including structural summaries, raw and range reads, sparse search snapshots, archive-aware reads/search, hashline-aware write/edit handling, and stronger tool guidance
- add benign stdio EPIPE guards across runtime and harness-host CLI entrypoints plus direct event writers so child teardown races stop surfacing as unhandled stream errors
- fix bundled runtime CLI startup by preventing workspace/composio MCP host modules from self-executing when imported through the main bundle
- fix the harness-host ESM bundle so CommonJS dependencies can resolve Node builtins like tty via createRequire during onboarding/session startup
- update runtime capability metadata to steer all agent sessions toward the intended read/search/find/edit/write workflow
- add and update focused regression coverage for hashline tool behavior and stdio guard handling
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix runtime/api-server run build
- validation: npm --prefix runtime/harness-host run build
- validation: npm --prefix runtime/harness-host run typecheck
- validation: npm --prefix apps/desktop run prepare:runtime:local
- validation: node out/runtime-macos/runtime/harness-host/dist/index.mjs

* fix: resume onboarding session after alignment approval

- rehydrate the stored onboarding session when alignment is approved and fail fast if the session id or backing session is missing
- enqueue an implementation handoff input, reset runtime state to QUEUED, and wake the queue worker so onboarding continues from the saved alignment report
- add regression coverage for the approval-to-implementation transition, including queued input context and worker wake behavior
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix runtime/api-server test -- --test-name-pattern "approving onboarding alignment resumes the onboarding session for implementation" (fails in this environment: Node ABI mismatch with node_modules/.bun/better-sqlite3)
- validation: bun test runtime/api-server/src/app.test.ts --test-name-pattern "approving onboarding alignment resumes the onboarding session for implementation" (fails in this environment: Bun does not support better-sqlite3)

* feat: improve onboarding implementation takeover

- show live delegated task pills during onboarding implementation using the existing background-task card pattern
- open delegated tasks in an in-place full-screen issue detail takeover and return back to onboarding instead of the board
- hide the experimentation shell sidebar while onboarding takeover is active so the onboarding surface uses the full shell width
- add focused source tests for the onboarding task strip, onboarding back override, and fullscreen shell takeover
- validation: bun --filter=holaboss-local run typecheck
- validation: node --test apps/desktop/src/components/panes/ChatPane.onboarding-implementation.test.mjs apps/desktop/src/components/layout/new-shell/IssueDetailPane.onboarding-back.test.mjs apps/desktop/src/components/layout/new-shell/NewAppShell.onboarding-fullscreen.test.mjs

* fix: strip workspace skills from front-session runtime requests

- filter workspace skill ids and descriptions out of direct main-session runtime config payloads
- suppress embedded and resolved workspace skill directories for front-session ts-runner harness requests
- keep subagent skill resolution coverage explicit in ts-runner tests so delegated sessions still retain workspace skills
- add runtime-config coverage proving main sessions no longer advertise workspace skills or the skill tool
- validation: npm --prefix runtime/api-server run typecheck
- validation: npm --prefix runtime/api-server test -- src/agent-runtime-config.test.ts src/ts-runner.test.ts (runs the package suite and still hits unrelated integration-env failures)

* fix: preserve onboarding alignment summary on implementation resume

- parse stored onboarding alignment reports before reading the approved summary during alignment approval resume flow
- tighten the stored alignment report helper to return the typed onboarding contract instead of a generic JSON value
- validation: bun --filter=@holaboss/runtime-api-server run typecheck
- validation: bun --filter=@holaboss/runtime-api-server run build
- validation: bun --filter=@holaboss/runtime-api-server run test (local-only cronjob timezone assertions expect UTC; the fixed onboarding approval path now passes and GitHub runners execute in UTC)

* fix: auto-continue deterministic onboarding without integrations

- trigger deterministic onboarding continuation automatically when context fetching starts with no tracked integration connections
- guard the auto-continue path per workspace and reset the guard when onboarding state is cleared
- extend the onboarding surface source test to cover the no-connections auto-continue flow
- Validation: `node --test apps/desktop/src/features/workspace-onboarding/DeterministicWorkspaceOnboardingSurface.test.mjs`
- add MiniMax-M3 to the desktop provider model catalog (minimax_direct)
- include MiniMax-M3 in AuthPanel default models and set it as the default background task model
- update runtime api-server background-task-model defaults to use MiniMax-M3 for minimax_direct
- update model-configuration docs to reflect the new minimax_direct default
- retain MiniMax-M2.7 and MiniMax-M2.7-highspeed as available alternatives
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants