diff --git a/frontend/src/i18n/generated/catalog.json b/frontend/src/i18n/generated/catalog.json index 4ca1a58..ed45223 100644 --- a/frontend/src/i18n/generated/catalog.json +++ b/frontend/src/i18n/generated/catalog.json @@ -125,8 +125,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 51, - "column": 1 + "line": 50, + "column": 48 } ] }, @@ -149,8 +149,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 338, - "column": 1 + "line": 337, + "column": 65 } ] }, @@ -366,8 +366,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 65, - "column": 1 + "line": 64, + "column": 48 } ] }, @@ -483,8 +483,8 @@ "refs": [ { "file": "src/components/ModelAdapterTestCard.vue", - "line": 146, - "column": 1 + "line": 145, + "column": 7 } ] }, @@ -531,8 +531,8 @@ "refs": [ { "file": "src/components/ModelAdapterTestCard.vue", - "line": 125, - "column": 1 + "line": 124, + "column": 60 } ] }, @@ -604,8 +604,8 @@ }, { "file": "src/components/CursorAccountCard.vue", - "line": 188, - "column": 1 + "line": 187, + "column": 11 } ] }, @@ -1473,8 +1473,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 390, - "column": 1 + "line": 389, + "column": 65 } ] }, @@ -1750,8 +1750,8 @@ "refs": [ { "file": "src/components/HomeMetricsCard.vue", - "line": 342, - "column": 1 + "line": 341, + "column": 23 } ] }, @@ -2666,8 +2666,8 @@ "refs": [ { "file": "src/components/CursorAccountCard.vue", - "line": 170, - "column": 1 + "line": 169, + "column": 53 } ] }, @@ -2777,8 +2777,8 @@ "refs": [ { "file": "src/views/Config.vue", - "line": 77, - "column": 1 + "line": 76, + "column": 48 } ] }, @@ -2854,8 +2854,8 @@ "refs": [ { "file": "src/components/CursorAccountCard.vue", - "line": 173, - "column": 1 + "line": 172, + "column": 81 } ] }, diff --git a/internal/backend/host.go b/internal/backend/host.go index 3c84d53..3ab2993 100644 --- a/internal/backend/host.go +++ b/internal/backend/host.go @@ -691,6 +691,20 @@ func (host *Host) rebuildLocked(cfg serverconfig.Config) error { return nil }), ), + // The always-local extension probes NetworkService/IsConnected roughly 10s + // after any slow request starts. A 404 here is treated as "network + // disconnected" and aborts in-flight work (e.g. commit message + // generation) even while the model is still streaming. Always answer OK. + server.POST("/aiserver.v1.NetworkService/IsConnected", + server.Name("network_is_connected"), + server.ConnectUnary(), + server.Local(upstream.MockProtoAction(routeDeps, upstream.CompatRouteConfig{ + Name: "network_is_connected", + StatusCode: http.StatusOK, + MockProtoType: "aiserver.v1.IsConnectedResponse", + MockBuilder: upstream.EmptyMockBuilder, + })), + ), server.Any("/aiserver.v1.NetworkService/*", server.Name("network_service"), server.HTTP(), diff --git a/internal/backend/server/upstream/client.go b/internal/backend/server/upstream/client.go index dfedcd8..bc23bb7 100644 --- a/internal/backend/server/upstream/client.go +++ b/internal/backend/server/upstream/client.go @@ -451,6 +451,8 @@ func newProtoMessage(typeName string) (proto.Message, error) { return &aiserverv1.SubmitLogsResponse{}, nil case "aiserver.v1.TrackEventsResponse": return &aiserverv1.TrackEventsResponse{}, nil + case "aiserver.v1.IsConnectedResponse": + return &aiserverv1.IsConnectedResponse{}, nil default: return nil, fmt.Errorf("unsupported proto message type %q", typeName) } diff --git a/internal/cursor/state_db.go b/internal/cursor/state_db.go index 940c0f1..e5ca304 100644 --- a/internal/cursor/state_db.go +++ b/internal/cursor/state_db.go @@ -33,6 +33,14 @@ var cursorStateDisabledStatsigGates = []string{ "disable_terminal_output_ui_streaming", } +// cursorStateEnabledStatsigGates forces gates to true. Disabling the network +// change monitor stops the always-local extension from probing +// NetworkService/IsConnected and aborting slow in-flight requests (such as +// commit message generation) with "Network disconnected". +var cursorStateEnabledStatsigGates = []string{ + "disable_network_change_monitor_local", +} + // InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the // Settings page. It does not modify the installed Cursor app bundle. func InjectCursorUserInfo(email, token string) error { @@ -50,12 +58,13 @@ func InjectCursorUserInfo(email, token string) error { } logger.Infof( - "injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s", + "injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s enabled_statsig_gates=%s", stateDBPath, values["cursorAuth/cachedEmail"], values["cursorAuth/stripeMembershipType"], values["cursorAuth/stripeSubscriptionStatus"], strings.Join(cursorStateDisabledStatsigGates, ","), + strings.Join(cursorStateEnabledStatsigGates, ","), ) return nil } @@ -120,7 +129,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error { } } - if err := disableCursorStatsigGates(ctx, tx); err != nil { + if err := syncCursorStatsigGateOverrides(ctx, tx); err != nil { return err } @@ -131,7 +140,7 @@ func syncCursorAuthStateDB(path string, values map[string]string) error { return nil } -func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { +func syncCursorStatsigGateOverrides(ctx context.Context, tx *sql.Tx) error { var raw []byte err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw) if err != nil { @@ -154,9 +163,15 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { hashUsed, _ := payload["hash_used"].(string) for _, gate := range cursorStateDisabledStatsigGates { - disableCursorStatsigGate(featureGates, gate) + setCursorStatsigGate(featureGates, gate, false, "local_disabled") if strings.EqualFold(hashUsed, "djb2") { - disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate)) + setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), false, "local_disabled") + } + } + for _, gate := range cursorStateEnabledStatsigGates { + setCursorStatsigGate(featureGates, gate, true, "local_enabled") + if strings.EqualFold(hashUsed, "djb2") { + setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), true, "local_enabled") } } @@ -170,21 +185,21 @@ func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error { return nil } -func disableCursorStatsigGate(featureGates map[string]any, key string) { +func setCursorStatsigGate(featureGates map[string]any, key string, value bool, rule string) { gate, _ := featureGates[key].(map[string]any) if gate == nil { gate = map[string]any{ "name": key, - "rule_id": "local_disabled", - "ruleID": "local_disabled", - "group_name": "local_disabled", - "groupName": "local_disabled", + "rule_id": rule, + "ruleID": rule, + "group_name": rule, + "groupName": rule, "id_type": "userID", "idType": "userID", } featureGates[key] = gate } - gate["value"] = false + gate["value"] = value } func cursorStateDJB2Hash(value string) string { diff --git a/proto/aiserver_v1.proto b/proto/aiserver_v1.proto index f540b19..51ef34b 100644 --- a/proto/aiserver_v1.proto +++ b/proto/aiserver_v1.proto @@ -17785,6 +17785,14 @@ message IsAllowedFreeTrialUsageResponse { bool is_allowed = 1; } +// Copied from: local:aiserver.v1.IsConnectedRequest +message IsConnectedRequest { +} + +// Copied from: local:aiserver.v1.IsConnectedResponse +message IsConnectedResponse { +} + // Copied from: local:aiserver.v1.IsCursorPredictionEnabledRequest (var: An) message IsCursorPredictionEnabledRequest { } @@ -36919,6 +36927,11 @@ service MetricsService { rpc ReportGauge(ReportMetricsRequest) returns (ReportMetricsResponse) {} } +// Source: aiserver.v1.NetworkService +service NetworkService { + rpc IsConnected(IsConnectedRequest) returns (IsConnectedResponse) {} +} + // Source: aiserver.v1.PerformanceEventService (var: i) service PerformanceEventService { rpc SubmitPerformanceEvents(SubmitPerformanceEventsRequest) returns (SubmitPerformanceEventsResponse) {}