feat: upgrade MiniMax default model to M3#417
Closed
octo-patch wants to merge 611 commits into
Closed
Conversation
- 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`
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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— addMiniMax-M3entry to theminimax_directcatalog (placed before M2.7 so it is offered first)apps/desktop/src/components/auth/AuthPanel.tsx— addMiniMax-M3tominimax_direct.defaultModelsand switchdefaultBackgroundModeltoMiniMax-M3runtime/api-server/src/background-task-model.ts— changeBACKGROUND_TASK_MODEL_DEFAULTS.minimax_directfromMiniMax-M2.7toMiniMax-M3website/docs/content/docs/contribute/desktop/model-configuration.mdx— update the documentedminimax_directbackground-task defaultWhy
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.tsunderruntime/api-server/— 5/5 tests pass