Skip to content

Commit 6410db1

Browse files
authored
feat(thu-fullrun): overlay attention, skills sync, credits & settings refresh (tinyhumansai#479)
* Update Conversations component to enhance user messaging for budget limits. Changed the warning text for exhausted weekly inference budget to improve clarity and user experience. * feat(schemas): add new configuration option for vision model usage - Introduced a new optional boolean field `use_vision_model` in the schemas for enabling vision LLM for screenshot analysis. - Updated the screen intelligence schemas to include a required `consent` field for starting sessions, replacing the previous `sample_interval_ms` field. - Enhanced the `ttl_secs` field description for clarity and modified the `capture_policy` to `screen_monitoring` for better understanding of its purpose. * feat(CoreStateProvider): enhance state management with optimistic updates and error handling - Implemented optimistic local commits for `setAnalyticsEnabled` and `setOnboardingCompletedFlag` to provide instant UI feedback while ensuring state consistency through authoritative snapshot refreshes. - Added error handling for the `refresh` function calls in `setAnalyticsEnabled`, `setOnboardingCompletedFlag`, and `clearSession` to log failures, improving robustness in state management during user interactions. - Updated dependencies in the `useCallback` hooks to include `refresh`, ensuring proper state updates and synchronization with the core. * feat(paths): centralize runtime path resolution for user-scoped skills data - Introduced a new module `paths.rs` to handle the resolution of runtime paths for skills, ensuring that `skills_data` and `workspace` directories are scoped per user. - Updated `bootstrap_skill_runtime` and `bootstrap_skills_runtime` functions to utilize the new path resolution logic, improving consistency and clarity in directory management. - Enhanced error logging for directory creation failures to include the specific path that failed, aiding in debugging. - Added a new optional field `overlay_ttl_ms` in the autocomplete schemas to support overlay time-to-live configuration. * feat(ScreenIntelligencePanel): optimize config synchronization to prevent user edit clobbering - Introduced a reference to track the last synced configuration signature, ensuring that user edits are preserved during periodic updates from the CoreStateProvider. - Updated the effect to compare serialized configuration values, allowing for re-sync only when actual changes occur, enhancing user experience and preventing unintended data loss. * feat(SkillManager): implement initial sync after OAuth completion - Added functionality to trigger an initial data sync immediately after OAuth completion, ensuring users see fresh data without waiting for the next scheduled sync. - Updated comments to clarify the change in sync behavior due to recent modifications in the Rust core, which no longer auto-triggers sync on OAuth completion. * fix(UsageLimitModal, Conversations): enhance user messaging for budget limits - Updated warning messages in both UsageLimitModal and Conversations components to provide clearer information regarding weekly limits and reset times. - Improved clarity in user notifications to enhance overall experience when budget limits are reached. * refactor(SkillSetupModal): improve session mode handling for skill configuration - Updated the SkillSetupModal component to lock the mode at mount time, ensuring users remain in the setup wizard during their session even if the skill is marked as complete. - Simplified mode management by replacing the forceSetup state with a sessionMode state, allowing explicit mode switching while maintaining a consistent user experience. * feat(SkillManager): enhance setup flow for OAuth-based skills - Updated the `startSetup` method to handle OAuth-based skills more effectively by implementing a fallback to core RPC for skills without a frontend runtime. - Improved error handling to treat missing `onSetupStart` implementations as successful completion for pure OAuth skills, allowing the setup wizard to display the "Connected!" screen. - Added detailed logging for both local runtime and core RPC fallback scenarios to improve traceability during the setup process. * feat(Home): enhance local AI status handling and asset management - Introduced a new state for local AI assets, allowing for better tracking of model file readiness. - Updated the loading logic to fetch both local AI status and assets concurrently, improving performance and error handling. - Implemented a mechanism to hide the Local Model Runtime card once all models are fully downloaded, enhancing user experience. - Added comprehensive comments to clarify the logic behind model readiness checks based on asset states. * refactor(Credits): update credit balance structure and terminology - Renamed credit categories in the RewardsCouponSection and PayAsYouGoCard components for clarity, changing "General credits" to "Promo credits" and "Top-up credits" to "Team top-up." - Updated the credit balance API to reflect the new structure, replacing `balanceUsd` and `topUpBalanceUsd` with `promotionBalanceUsd` and `teamTopupUsd`. - Adjusted normalization logic in the credits API to accommodate the new credit balance fields. - Modified tests to ensure correct handling of the updated credit balance structure. * feat(Settings): reorganize billing settings and update descriptions - Added a new top-level billing section to the settings, promoting it out of the Account & Security category for better visibility. - Updated the description for the Account & Security section to remove billing references, focusing on recovery phrase, team management, and linked account access. - Adjusted the settings navigation to accommodate the new billing section, ensuring proper routing and user experience. * refactor(Config): change logging level from info to debug for environment overrides - Updated logging statements in the Config implementation to use debug level instead of info, reducing verbosity during runtime while maintaining necessary traceability for configuration loading. * feat(Overlay): implement overlay attention event handling and refactor overlay app structure - Introduced a new overlay module to manage attention events, allowing the core to publish messages to the overlay window. - Enhanced the OverlayApp component to handle dictation and attention events, improving user interaction with the overlay. - Refactored the overlay state management to support different modes (idle, stt, attention) and added auto-dismiss functionality for attention messages. - Removed the Browser Access Toggle from the Skills page, streamlining the UI and focusing on core functionalities. - Updated tests to reflect changes in the Skills component and removed unnecessary mocks related to browser access. * fix(OverlayBubbleChip): reset typewriter animation on new bubble identity - Updated the OverlayBubbleChip component to reset the typewriter animation correctly when a new bubble is displayed by using the `key` prop. - Refactored the cleanup logic in the useEffect hook to ensure proper interval management and state reset, enhancing the user experience with bubble transitions. * refactor(rest): streamline key_bytes_from_string function and improve readability - Simplified the condition for checking the ASCII key length and character restrictions in the key_bytes_from_string function. - Consolidated the import statements for base64 engines into a single line for better clarity. - Adjusted test data formatting for improved readability in the key_bytes_from_string_tests module. * enhance(logging): improve color detection logic for terminal output - Updated the color detection logic in the logging module to prioritize environment variables (`NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`) for better control over color output. - Added detailed comments explaining the color resolution order, enhancing code clarity and maintainability. * test(Home): add mock for openhumanLocalAiAssetsStatus in tests - Enhanced the Home and HomeBootstrapButtons test files by adding a mock implementation for openhumanLocalAiAssetsStatus, which resolves to an object with null result and empty logs. This improves the test setup for local AI asset status handling. * refactor(SkillSetupModal): improve session mode handling and loading state - Updated the SkillSetupModal component to ensure session mode is determined after the first snapshot resolution, preventing premature defaults to the setup wizard. - Introduced a loading state to display a message while waiting for the skill setup status, enhancing user experience during the modal's initial render. - Refactored the SkillManager to throw errors for real failures during setup, ensuring proper error handling and user feedback. * refactor(Config): simplify logging for invalid proxy scope values - Consolidated the logging statement for invalid OPENHUMAN_PROXY_SCOPE values into a single line, improving code readability while maintaining the warning functionality.
1 parent 6465f3d commit 6410db1

39 files changed

Lines changed: 1150 additions & 332 deletions

app/src-tauri/src/core_process.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::io::IsTerminal;
12
use std::path::PathBuf;
23
use std::sync::Arc;
34

@@ -7,6 +8,23 @@ use tokio::sync::Mutex;
78
use tokio::task::JoinHandle;
89
use tokio::time::{timeout, Duration};
910

11+
/// Propagate ANSI color hints to the spawned core child.
12+
///
13+
/// Core's tracing formatter auto-detects color via `stderr.is_terminal()`,
14+
/// but when core runs as a grandchild under `yarn tauri dev` the inherited
15+
/// stderr may not register as a TTY even though the ultimate terminal
16+
/// supports ANSI. If the Tauri process itself is attached to a TTY we
17+
/// forward `FORCE_COLOR=1` so core emits colored log lines; `NO_COLOR`
18+
/// (user opt-out) always wins and short-circuits the propagation.
19+
fn apply_core_color_env(cmd: &mut Command) {
20+
if std::env::var_os("NO_COLOR").is_some() {
21+
return;
22+
}
23+
if std::io::stderr().is_terminal() {
24+
cmd.env("FORCE_COLOR", "1");
25+
}
26+
}
27+
1028
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1129
pub enum CoreRunMode {
1230
InProcess,
@@ -119,6 +137,7 @@ impl CoreProcessHandle {
119137
.arg(self.port.to_string());
120138
cmd
121139
};
140+
apply_core_color_env(&mut cmd);
122141
let child = cmd
123142
.spawn()
124143
.map_err(|e| format!("failed to spawn core process: {e}"))?;
@@ -156,6 +175,7 @@ impl CoreProcessHandle {
156175
cmd
157176
};
158177

178+
apply_core_color_env(&mut cmd);
159179
let child = cmd
160180
.spawn()
161181
.map_err(|e| format!("failed to spawn core process: {e}"))?;

app/src/components/rewards/RewardsCouponSection.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,18 @@ const RewardsCouponSection = () => {
163163
<div className="grid gap-4 sm:grid-cols-3">
164164
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
165165
<div className="text-xs font-medium uppercase tracking-wide text-stone-400">
166-
General credits
166+
Promo credits
167167
</div>
168168
<div className="mt-2 text-2xl font-semibold text-stone-900">
169-
{creditBalance ? formatUsd(creditBalance.balanceUsd) : loading ? '…' : '—'}
169+
{creditBalance ? formatUsd(creditBalance.promotionBalanceUsd) : loading ? '…' : '—'}
170170
</div>
171171
</div>
172172
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
173173
<div className="text-xs font-medium uppercase tracking-wide text-stone-400">
174-
Top-up credits
174+
Team top-up
175175
</div>
176176
<div className="mt-2 text-2xl font-semibold text-stone-900">
177-
{creditBalance ? formatUsd(creditBalance.topUpBalanceUsd) : loading ? '…' : '—'}
177+
{creditBalance ? formatUsd(creditBalance.teamTopupUsd) : loading ? '…' : '—'}
178178
</div>
179179
</div>
180180
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">

app/src/components/rewards/__tests__/RewardsCouponSection.test.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ describe('RewardsCouponSection', () => {
2828

2929
it('loads balances and refreshes history after a successful redemption', async () => {
3030
mocks.mockCreditsApi.getBalance
31-
.mockResolvedValueOnce({ balanceUsd: 3, topUpBalanceUsd: 1, topUpBaselineUsd: null })
32-
.mockResolvedValueOnce({ balanceUsd: 8, topUpBalanceUsd: 1, topUpBaselineUsd: null });
31+
.mockResolvedValueOnce({ promotionBalanceUsd: 3, teamTopupUsd: 1 })
32+
.mockResolvedValueOnce({ promotionBalanceUsd: 8, teamTopupUsd: 1 });
3333
mocks.mockCreditsApi.getUserCoupons
3434
.mockResolvedValueOnce([])
3535
.mockResolvedValueOnce([
@@ -70,11 +70,7 @@ describe('RewardsCouponSection', () => {
7070
});
7171

7272
it('shows backend redemption errors without clearing the existing state', async () => {
73-
mocks.mockCreditsApi.getBalance.mockResolvedValue({
74-
balanceUsd: 3,
75-
topUpBalanceUsd: 0,
76-
topUpBaselineUsd: null,
77-
});
73+
mocks.mockCreditsApi.getBalance.mockResolvedValue({ promotionBalanceUsd: 3, teamTopupUsd: 0 });
7874
mocks.mockCreditsApi.getUserCoupons.mockResolvedValue([]);
7975
mocks.mockCreditsApi.redeemCoupon.mockRejectedValueOnce({
8076
error: 'This coupon has already been used.',
@@ -94,8 +90,8 @@ describe('RewardsCouponSection', () => {
9490

9591
it('shows pending coupon copy and keeps the current balance until the reward is fulfilled', async () => {
9692
mocks.mockCreditsApi.getBalance
97-
.mockResolvedValueOnce({ balanceUsd: 3, topUpBalanceUsd: 0, topUpBaselineUsd: null })
98-
.mockResolvedValueOnce({ balanceUsd: 3, topUpBalanceUsd: 0, topUpBaselineUsd: null });
93+
.mockResolvedValueOnce({ promotionBalanceUsd: 3, teamTopupUsd: 0 })
94+
.mockResolvedValueOnce({ promotionBalanceUsd: 3, teamTopupUsd: 0 });
9995
mocks.mockCreditsApi.getUserCoupons
10096
.mockResolvedValueOnce([])
10197
.mockResolvedValueOnce([

app/src/components/settings/SettingsHome.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const SettingsHome = () => {
7979
{
8080
id: 'account',
8181
title: 'Account & Security',
82-
description: 'Billing, recovery phrase, team management, and linked account access',
82+
description: 'Recovery phrase, team management, and linked account access',
8383
icon: (
8484
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
8585
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5v14" />
@@ -88,6 +88,23 @@ const SettingsHome = () => {
8888
onClick: () => navigateToSettings('account'),
8989
dangerous: false,
9090
},
91+
{
92+
id: 'billing',
93+
title: 'Billing & Usage',
94+
description: 'Subscription plan, pay-as-you-go credits, and payment methods',
95+
icon: (
96+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
97+
<path
98+
strokeLinecap="round"
99+
strokeLinejoin="round"
100+
strokeWidth={2}
101+
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3 3v8a3 3 0 003 3z"
102+
/>
103+
</svg>
104+
),
105+
onClick: () => navigateToSettings('billing'),
106+
dangerous: false,
107+
},
91108
{
92109
id: 'automation',
93110
title: 'Automation & Channels',

app/src/components/settings/hooks/useSettingsNavigation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,11 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
155155
case 'ai-tools':
156156
return [settingsCrumb];
157157

158-
// Leaf panels under account
158+
// Top-level billing leaf (promoted out of Account & Security)
159159
case 'billing':
160+
return [settingsCrumb];
161+
162+
// Leaf panels under account
160163
case 'recovery-phrase':
161164
case 'team':
162165
case 'connections':

app/src/components/settings/panels/ScreenIntelligencePanel.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ComponentProps, useEffect, useMemo, useState } from 'react';
1+
import { type ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
22

33
import ScreenIntelligenceDebugPanel from '../../../components/intelligence/ScreenIntelligenceDebugPanel';
44
import { useScreenIntelligenceState } from '../../../features/screen-intelligence/useScreenIntelligenceState';
@@ -79,10 +79,21 @@ const ScreenIntelligencePanel = () => {
7979
const [isSavingConfig, setIsSavingConfig] = useState(false);
8080
const [configError, setConfigError] = useState<string | null>(null);
8181

82+
// CoreStateProvider polls every 2s (CoreStateProvider.tsx POLL_MS), producing a
83+
// new `status` object reference on every tick even when the underlying config is
84+
// unchanged. Keying this effect on `status?.config` identity would therefore
85+
// clobber in-progress user edits every 2 seconds. Compare the serialized value
86+
// instead, so we only re-sync when the server config has actually changed.
87+
const lastSyncedConfigSigRef = useRef<string | null>(null);
8288
useEffect(() => {
8389
if (!status?.config) {
8490
return;
8591
}
92+
const sig = JSON.stringify(status.config);
93+
if (lastSyncedConfigSigRef.current === sig) {
94+
return;
95+
}
96+
lastSyncedConfigSigRef.current = sig;
8697
setEnabled(status.config.enabled ?? false);
8798
setPolicyMode(
8899
status.config.policy_mode === 'whitelist_only' ? 'whitelist_only' : 'all_except_blacklist'

app/src/components/settings/panels/billing/PayAsYouGoCard.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ const PayAsYouGoCard = ({
2020
onTopUp,
2121
onBalanceRefresh,
2222
}: PayAsYouGoCardProps) => {
23-
const promoCredits = creditBalance?.balanceUsd ?? 0;
24-
const topUpCredits = creditBalance?.topUpBalanceUsd ?? 0;
25-
const topUpBaseline = creditBalance?.topUpBaselineUsd ?? null;
26-
const availableCredits = promoCredits + topUpCredits;
23+
// Backend `GET /payments/credits/balance` returns
24+
// { promotionBalanceUsd, teamTopupUsd }
25+
// `promotionBalanceUsd` lives on the user document
26+
// (`IUserUsage.promotionBalanceUsd`) and unifies signup bonus, coupons,
27+
// and referral rewards. `teamTopupUsd` is the team-level paid top-up pool.
28+
// Together they make the pay-as-you-go spendable balance.
29+
const promoCredits = creditBalance?.promotionBalanceUsd ?? 0;
30+
const teamTopupCredits = creditBalance?.teamTopupUsd ?? 0;
31+
const availableCredits = promoCredits + teamTopupCredits;
2732

2833
// Coupon state (local — no need to share with other sections)
2934
const [couponCode, setCouponCode] = useState('');
@@ -78,30 +83,11 @@ const PayAsYouGoCard = ({
7883
<span className="text-xs text-stone-400">Signup + promo credits</span>
7984
<span className="text-xs font-medium text-stone-900">${promoCredits.toFixed(2)}</span>
8085
</div>
81-
<div className="space-y-1">
82-
<div className="flex items-center justify-between">
83-
<span className="text-xs text-stone-400">Top-up credits</span>
84-
<span className="text-xs font-medium text-stone-900">
85-
${topUpCredits.toFixed(2)}
86-
{topUpBaseline != null && topUpBaseline > 0 && (
87-
<span className="text-stone-500 font-normal"> / ${topUpBaseline.toFixed(2)}</span>
88-
)}
89-
</span>
90-
</div>
91-
{topUpBaseline != null && topUpBaseline > 0 && (
92-
<div className="h-1 bg-stone-700/60 rounded-full overflow-hidden">
93-
<div
94-
className={`h-full rounded-full transition-all duration-300 ${
95-
topUpCredits <= 0
96-
? 'bg-coral-500'
97-
: topUpCredits / topUpBaseline < 0.2
98-
? 'bg-amber-500'
99-
: 'bg-primary-500'
100-
}`}
101-
style={{ width: `${Math.min(100, (topUpCredits / topUpBaseline) * 100)}%` }}
102-
/>
103-
</div>
104-
)}
86+
<div className="flex items-center justify-between">
87+
<span className="text-xs text-stone-400">Team top-up credits</span>
88+
<span className="text-xs font-medium text-stone-900">
89+
${teamTopupCredits.toFixed(2)}
90+
</span>
10591
</div>
10692
</div>
10793
) : isLoadingCredits ? (

app/src/components/skills/SkillSetupModal.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,29 @@ export default function SkillSetupModal({
3131
const modalRef = useRef<HTMLDivElement>(null);
3232
const snap = useSkillSnapshot(skillId);
3333
const setupComplete = snap?.setup_complete ?? false;
34-
// Track whether the user has explicitly chosen to reconfigure (setup mode)
35-
// even though setup is already complete.
36-
const [forceSetup, setForceSetup] = useState(false);
37-
// Derive mode: show manage if setup is complete (or no setup needed),
38-
// unless the user explicitly chose to reconfigure.
39-
const mode = forceSetup ? "setup" : (!hasSetup || setupComplete ? "manage" : "setup");
40-
const setMode = (m: "manage" | "setup") => setForceSetup(m === "setup");
34+
// Lock the mode in once we have a concrete snapshot — `useSkillSnapshot`
35+
// returns `null` on the first render while it fetches, so reading
36+
// `setup_complete` at mount time would always see `false` and wrongly
37+
// default an already-connected skill into the setup wizard.
38+
//
39+
// We keep `sessionMode` stable after the first resolution so that an
40+
// OAuth flow that flips `setup_complete` to true mid-wizard does not
41+
// yank the user out of the wizard's own "complete" success screen.
42+
// The user can still switch modes explicitly via `setMode` below
43+
// (e.g. SkillManagementPanel's "Reconfigure" button).
44+
const [sessionMode, setSessionMode] = useState<"manage" | "setup" | null>(
45+
() => (!hasSetup ? "manage" : null),
46+
);
47+
48+
useEffect(() => {
49+
if (sessionMode !== null) return;
50+
// Wait for the first concrete snapshot before deciding.
51+
if (snap === null) return;
52+
setSessionMode(setupComplete ? "manage" : "setup");
53+
}, [sessionMode, snap, setupComplete]);
54+
55+
const setMode = (m: "manage" | "setup") => setSessionMode(m);
56+
const mode = sessionMode;
4157

4258
// Handle escape key
4359
useEffect(() => {
@@ -71,7 +87,11 @@ export default function SkillSetupModal({
7187
};
7288

7389
const headerTitle =
74-
mode === "manage" ? `Manage ${skillName}` : `Connect ${skillName}`;
90+
mode === null
91+
? skillName
92+
: mode === "manage"
93+
? `Manage ${skillName}`
94+
: `Connect ${skillName}`;
7595

7696
const modalContent = (
7797
<div
@@ -142,7 +162,11 @@ export default function SkillSetupModal({
142162

143163
{/* Content */}
144164
<div className="p-4">
145-
{mode === "manage" ? (
165+
{mode === null ? (
166+
<div className="flex items-center justify-center py-8 text-sm text-stone-400">
167+
Loading…
168+
</div>
169+
) : mode === "manage" ? (
146170
<SkillManagementPanel
147171
skillId={skillId}
148172
onClose={onClose}

app/src/components/upsell/UsageLimitModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function UsageLimitModal({
4343
if (!open) return null;
4444

4545
const bodyText = isBudgetExhausted
46-
? 'Your included weekly inference budget is exhausted. Upgrade your plan or top up credits to continue.'
46+
? `You've hit your weekly limit.${resetTime ? ` It resets ${formatResetTime(resetTime)}.` : ''} Upgrade your plan or top up credits to avoid limits.`
4747
: `You've hit your 10-hour inference rate limit.${resetTime ? ` It resets ${formatResetTime(resetTime)}.` : ''} Upgrade for higher limits.`;
4848

4949
return (

app/src/lib/skills/manager.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,18 +148,59 @@ class SkillManager {
148148
/**
149149
* Start the setup flow for a skill. Returns the first step, or null if
150150
* the skill doesn't implement setup/start (e.g. OAuth-only skills).
151+
*
152+
* OAuth-based skills never instantiate a frontend `SkillRuntime` — they
153+
* live only in the Rust core (see `shared.tsx` "no need to start the
154+
* QuickJS runtime first"). For those we fall back to the core RPC
155+
* `openhuman.skills_setup_start`, mirroring the fallback pattern in
156+
* `notifyOAuthComplete`. If the core errors (e.g. the skill doesn't
157+
* implement `onSetupStart` at all, which is common for pure OAuth
158+
* skills), we treat it as "no more setup steps" — the wizard interprets
159+
* that as success and shows the "Connected!" screen.
151160
*/
152161
async startSetup(skillId: string): Promise<SetupStep | null> {
153162
console.log("[SkillManager] startSetup", skillId);
154163
const runtime = this.runtimes.get(skillId);
155-
if (!runtime) {
156-
console.log("[SkillManager] runtime not found", skillId);
157-
throw new Error(`Skill ${skillId} runtime not found`);
164+
if (runtime) {
165+
emitSkillStateChange(skillId);
166+
console.log("[SkillManager] setup started (local runtime)", skillId);
167+
return runtime.setupStart();
158168
}
159169

160-
emitSkillStateChange(skillId);
161-
console.log("[SkillManager] setup started", skillId);
162-
return runtime.setupStart();
170+
// No frontend runtime — dispatch via core RPC pass-through.
171+
//
172+
// The core side returns `null` (not an error) when the skill has no
173+
// `onSetupStart` handler — see `handle_js_call` in
174+
// `src/openhuman/skills/qjs_skill_instance/js_handlers.rs`, which
175+
// falls through to `return "null"` when the function is missing.
176+
// So any exception thrown here is a *real* failure (skill not
177+
// running, RPC transport error, JS exception in the handler, …)
178+
// and must be propagated so the caller can surface it to the user
179+
// instead of silently pretending setup succeeded.
180+
try {
181+
const result = (await callCoreRpc({
182+
method: "openhuman.skills_setup_start",
183+
params: { skill_id: skillId },
184+
})) as { step?: SetupStep } | null;
185+
emitSkillStateChange(skillId);
186+
console.log(
187+
"[SkillManager] setup started (core RPC fallback)",
188+
skillId,
189+
result,
190+
);
191+
if (!result || !result.step) {
192+
return null;
193+
}
194+
return result.step;
195+
} catch (err) {
196+
console.warn(
197+
"[SkillManager] setup_start core fallback failed",
198+
skillId,
199+
err,
200+
);
201+
emitSkillStateChange(skillId);
202+
throw err;
203+
}
163204
}
164205

165206
/**
@@ -344,6 +385,22 @@ class SkillManager {
344385
}
345386
}
346387

388+
// Kick off an initial sync so the user sees fresh data immediately
389+
// after connecting, rather than waiting for the next cron tick.
390+
// The Rust core no longer auto-triggers sync on oauth/complete
391+
// (removed in commit 840b1d3c), so the frontend drives it here.
392+
// Fire-and-forget: any failure is logged but must not block the
393+
// OAuth completion flow.
394+
try {
395+
console.log(`[SkillManager] kicking initial sync after OAuth for '${skillId}'`);
396+
await this.triggerSync(skillId);
397+
} catch (syncErr) {
398+
console.warn(
399+
`[SkillManager] initial post-OAuth sync failed for '${skillId}':`,
400+
syncErr,
401+
);
402+
}
403+
347404
emitSkillStateChange(skillId);
348405
}
349406

0 commit comments

Comments
 (0)